Djangoでタスク管理アプリをつくりたい！ プロジェクト管理編

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

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

# プロジェクト管理

## ナビゲーションドロワーの作成
### 前準備
jQueryをつかうので，[公式](http://jquery.com/download/)からダウンロードして，`static`フォルダ内に`js`フォルダを作りそこに格納する


下記のJavaScriptとCSSを作成する

```javascript:navibar.js
$(function($) {
    WindowHeight = $(window).height()

    $(document).ready(function() {
        $('.barbtn').click(function(){
            if($('.drawr').is(":animated")){
                return false;
            }else{
                $('.drawr').animate({width:'toggle'});
                $(this).toggleClass('peke');
                return false;
            }
        });
    });

    //別領域をクリックでメニューを閉じる
    $(document).click(function(event) {
        if (!$(event.target).closest('.drawr').length) {
            $('.barbtn').removeClass('peke');
            $('.drawr').hide();
        }
    });

});
```


```CSS:navibar.css

.peke {
    background-position: -35px 0;
}
.drawr {
    display: none;
    background-color:rgba(0, 0, 0, 0.404);
    position: absolute;
    top: 0px;
    left:0;
    width:250px;
    padding:60px 0 20px 20px;
    z-index: 100;
}

.barmenu{
    width:220px;
    height:50px;
    margin-top:10px;
}

.barmenu a{
    position: relative;
	display: block;
	font-weight: bold;
	text-decoration: none;
	color: #FFF;
	text-shadow: 0 0 5px rgba(255, 255, 255, 0.0);
	transition: .2s;
}

.barmenu a:hover {
    text-shadow: -6px 0px 15px rgba(255, 255, 240, 0.83), 6px 0px 15px rgba(255, 255, 240, 0.83);
	transition: .2s;
}

.top{
    font-size: 32px;
    position: relative;
	display: block;
	font-weight: bold;
	text-decoration: none;
    color: #FFF;
    text-shadow: -6px 0px 15px rgba(5, 51, 255, 0.83), 6px 0px 15px rgba(255, 255, 240, 0.83);
    height: 100px;
}
```

### HTML修正
JavaScriptとCSSを元に`base.html`を修正する

```html:base.html
<!DOCTYPE html>
<html lang='ja'>
    <head>
        {% load staticfiles %}
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">

        <link rel="shortcut icon" type="image/x-icon" href="{% static 'img/c0063_6_2.png' %}">

        <title>{% block title %}{% endblock %}</title>

        <link href="{% static 'bootstrap/dist/css/bootstrap.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-grid.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-grid.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-reboot.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-reboot.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/docs/4.0/examples/dashboard/dashboard.css' %}" rel="stylesheet">
        
        <link href="{% static 'css/adjustment.css' %}" rel="stylesheet" type="text/css">
        <link href="{% static 'css/navibar.css' %}" rel="stylesheet" type="text/css">  <!-- 追加 -->

        <script type="text/javascript" src="{% static 'js/jquery-3.3.1.min.js' %}"></script>  <!-- 追加 -->

        <script src="{% static 'bootstrap/assets/js/vendor/anchor.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/clipboard.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/holder.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/popper.min.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.bundle.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.bundle.min.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.min.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/alert.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/button.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/carousel.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/collapse.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/dropdown.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/index.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/modal.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/scrollspy.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/tab.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/tooltip.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/util.js' %}"></script>

        <script src="{% static 'js/navibar.js' %}"></script>  <!-- 追加 -->
    </head>

    <body>
        <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
            <a class="navbar-brand col-sm-3 col-md-2 mr-0 barbtn" href="">TaskMan</a>  <!-- 変更 -->
            <div class="form-control form-control-dark w-100">{% block mainT %}{% endblock %}</div> 
            <div class="btn-group">
                <div class="btn dropdown-toggle naviDrop" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        {% if user.is_authenticated %}
                            User : {{ user.get_username }}
                            {% if user.icon.url != "" %}
                                <img src="{{ user.icon.url }}" style="height: 20px; width: 20px;">
                            {% else %}
                                <img src="/static/img/c0063_6_2.png" style="height: 20px; width: 20px;">
                            {% endif%}
                        {% else %}
                            User
                        {% endif %}
                </div>
                {% if user.is_authenticated %}
                    <div class="dropdown-menu dropdown-menu-right" >
                    <a class="dropdown-item" href="{% url 'accounts:profile' user.pk %}">ユーザ情報</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="/logout/">ログアウト</a>
                    </div>
                {% endif %}
            </div>
        </nav>

　　　 <!-- 追加 start -->
        <div class="drawr">
            <div class="barmenu top">TaskManager</div>
            <div class="barmenu"><a href="/">トップページ</a></div>
            <div class="barmenu"><a href="#">タスク</a></div>
            <div class="barmenu"><a href="#">ガントチャート</a></div>
            <div class="barmenu"><a href="#">チャット</a></div>
        </div>
      <!-- 追加 end -->
    
        {% block body %}{% endblock %}

    </body>
</html>
```

### 画面確認
・メニュー画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/cdc58473-06ad-154f-c96f-d0536ecc9c27.png)

右上の`TaskMan`を押すとナビゲーションドロワーが表示されるのを確認する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/84d3bdf9-6672-3455-7ada-d308580f7b29.png)


## プロジェクトの作成
### modelsの作成
プロジェクトごとにタスクを管理していきたいので，プロジェクトのモデルを作成する

```python:task/models.py
from django.db import models
from accounts.models import User


""" プロジェクトモデル """
class Project(models.Model):
    project_cd = models.AutoField(
        primary_key=True
    )

    name = models.CharField(
        max_length=25,
        verbose_name='プロジェクト名'
    )

    leader = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='leader',
        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='詳細'
    )

    update_date = models.DateField(
        auto_now=True
    )

    is_delete = models.BooleanField(
        default=False
    )
```

また，プロジェクトとユーザ，プロジェクトとタスクを紐づけるモデルを作成

```python:models.py
""" プロジェクト To ユーザ （1 対 多） """
class ProjectToUsers(models.Model):
    project_cd = models.ForeignKey(
        Project,
        on_delete=models.CASCADE
    )

    user_cd = models.ForeignKey(
        User,
        on_delete=models.CASCADE
    )
```

## プロジェクト画面の作成
タスクをまとめるプロジェクト一覧用の画面を作成する

### URL設定
プロジェクトのurls.pyを編集する

```python:urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('accounts.urls')),
    path('task/', include('task.urls')),  # 追加
]
```

この設定により`task/`で始まるURLは`taskアプリケーション`の`urls.py`を参照するようになる
なので，`task`の`urls.py`を編集する

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

app_name = 'task'

urlpatterns = [
    path('', views.taskTop.as_view(), name='task_top'),
]
```

`views.py'も編集する

```python:views.py
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views import generic
from django.contrib.sites.shortcuts import get_current_site
from django.shortcuts import redirect, resolve_url
from django.contrib.auth.decorators import login_required
from django.contrib.auth import get_user_model
from django.db import models
from .models import ProjectToUsers, Project

User = get_user_model()


""" トップページ """
class taskTop(LoginRequiredMixin, generic.TemplateView):
    template_name = 'task/task_top.html'
    redirect_field_name = 'redirect_to'

    def get_context_data(self, **kwargs):
        user = self.request.user
        context = super().get_context_data(**kwargs)

        project_user = ProjectToUsers.objects.filter(user_cd=user.use_cd)
        leader = Project.objects.filter(leader=user.use_cd, is_delete=0)

        if len(project_user) > 0:
            context['member'] = []
            for person in project_user:
                member = Project.objects.filter(project_cd=person.project_cd.pk, is_delete=0)
                context['member'].extend(member)
        else:
            context['member'] = None

        context['leader'] = leader if len(leader) > 0 else None

        return context
```

ユーザがリーダのプロジェクトと，ユーザが参加しているプロジェクトを取得しhtmlに渡す処理を行う

### HTML作成
templatesファイルにhtmlを作成する

```html:task/task_top.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} タスク {% endblock %}
{% block body %}

    <div class="leftArea">
        <h2 class="eleTitle">主導プロジェクト</h2>
        {% if leader != None %}
            {% for lead in leader %}
                <a class="project_area" href="{% url 'task:our_project' lead.pk %}">
                    <div>
                        <h4>{{lead.name}}</h4>
                        <p>リーダ : {{lead.leader}}</p>
                        {% if lead.start_date != None %}
                            <p>開始日 : {{lead.start_date}}</p>
                        {% endif %}
                        {% if lead.end_date != None %}
                            <p>終了日 : {{lead.end_date}}</p>
                        {% endif %}
                        {% if lead.details != None %}
                            <p>詳細 : {{lead.details}}</p>
                        {% endif %}
                    </div>
                </a>
            {% endfor %}
        {% else %}
            <h4>プロジェクトがありません</h4>
        {% endif %}
        <a class="btn btn-outline-secondary" href="/task/build_project/">プロジェクト作成</a>
    </div>
    <div class="rightArea">
        <h2 class="eleTitle">参加プロジェクト</h2>
        {% if member != None %}
            {% for men in member %}
                <a class="project_area" href="{% url 'task:our_project' men.pk %}">
                    <div>
                        <h4>{{men.name}}</h4>
                        <p>リーダ : {{men.leader}}</p>
                        {% if men.start_date != None %}
                            <p>開始日 : {{men.start_date}}</p>
                        {% endif %}
                        {% if men.end_date != None %}
                            <p>終了日 : {{men.end_date}}</p>
                        {% endif %}
                        {% if men.details != None %}
                            <p>詳細 : {{men.details}}</p>
                        {% endif %}
                    </div>
                </a>
            {% endfor %}
        {% else %}
            <h4>プロジェクトがありません</h4>
        {% endif %}
    </div>
{% endblock %}
```

cssを追加する

```css:adjustment.css
.leftArea {
    width: 50%;
    height: 100%;
    padding: 20px;
    border-right: 1px;
    float:left;
    word-wrap: break-word;
}


.rightArea {
    width: 50%;
    height: 100%;
    padding: 20px;
    float: right;
    word-wrap: break-word;
}

.eleTitle {
    padding-bottom:10px;
    border-bottom: solid 5px rgb(36, 0, 119);
}

.project_area {
    color:black;
}

.project_area:hover{
    text-decoration:  none;
}

.project_area div{
    border-radius:20px;
    padding:10px;
    margin-bottom:20px;
    width: 80%;
    background-color: rgb(235, 244, 255);
    -webkit-transition: all 0.5s;
    transition: all 0.5s;
}

.project_area div:hover{
    background-color: #c3dfff; /*ボタン色*/
}

.project_area h4{
    border-bottom: solid 3px rgb(3, 0, 161);
}
```
画面の作成が完了

### 画面確認
 ![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/44ef0a61-8920-e792-a46d-d5de6cefb61d.png)

`タスク`を選択すると以下の画面が表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/4fb8c769-8acb-9538-90db-78b85ed556db.png)

## 新規プロジェクトの作成
次はタスクを管理するプロジェクトを作成する処理を作る

### FORM・URL設定
登録用のフォームとURLを作成する

```python:task/forms.py
from django import forms
from .models import Project, ProjectToUsers

""" プロジェクト作成 """
class ProjectCreate(forms.ModelForm):

    class Meta:
        model = Project
        fields = (
            'name', 'leader',
            'start_date', 'end_date',
            'details'
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
        self.fields['leader'].widget.attrs['hidden'] = 'true'
        self.fields['start_date'].widget.input_type = "date"
        self.fields['end_date'].widget.input_type="date"
```

登録者をリーダとするので，`leader`は変更できないように`hidden`にし，日付は`date型にしてカレーコントロールが出来るようにする
（date型のカレンダーコントロールはIE非対応）

```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'),  # 追加
]
```

```python:views.py
from .forms import ProjectCreate



""" プロジェクト作成 """
class BuildProject(LoginRequiredMixin, generic.CreateView):
    model = Project
    form_class = ProjectCreate
    success_url ='/task/'
    template_name = 'task/build_project.html'

    def get_initial(self): 
        leader = self.request.user.pk
        return {'leader': leader,}
```
`def get_initial`で`leader`の初期値を作成者にする


### 画面確認
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/ffdd8746-5f2c-2720-11ce-322b1c768fe9.png)

プロジェクト一覧画面から作成ボタンを押す
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/52070738-9209-769e-52f3-c9decf904983.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/39f67e4e-45d6-506d-c5c8-61a62516d9af.png)
入力して登録をすると一覧画面に表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/9d779d79-3c83-d9bd-49e9-477dee269b8d.png)

## プロジェクトページの作成
作成したプロジェクトのタスクやメンバーなどを表示するページを作成する

### URL設定
ページのURLを設定する

```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'),  # 追加
```

```python:views.py

""" プロジェクト限定 """
class ProjectUserOnlyMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        is_return = True

        user = self.request.user

        l_result = Project.objects.filter(leader=user.use_cd, project_cd=self.kwargs["pk"])
        m_result = ProjectToUsers.objects.filter(project_cd=self.kwargs["pk"], user_cd=self.request.user.pk)
        p_result = Project.objects.filter(project_cd=self.kwargs["pk"], is_delete=0)

        if len(l_result) == 0 and len(m_result) == 0:
            is_return = False

        if len(p_result) == 0:
            is_return = False

        return is_return

""" プロジェクトページ """
class ProjectPage(ProjectUserOnlyMixin, generic.TemplateView):
    template_name = 'task/our_project.html'

    def get_context_data(self, **kwargs):
        user = self.request.user
        context = super().get_context_data(**kwargs)

        project = Project.objects.filter(project_cd=self.kwargs["pk"])
        p_user = ProjectToUsers.objects.filter(project_cd=self.kwargs["pk"])

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

        context['project'] = project if len(project) > 0 else None
        context['task'] = None

        return context
```

プロジェクトに参加していないメンバーが入れないように`UserPassesTestMixin`を継承した`ProjectUserOnlyMixin`を作成し参加しているメンバーだけに画面が表示されるようにする

`get_context_data`でプロジェクトの情報をtemplatesHTMLに渡す

### HTML作成
プロジェクト画面のHTMLを作成する

```html:our_project.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %}  {{ project.0.name }} {% endblock %}
{% block body %}

<nav class=" navbar-dark bg-dark flex-md-nowrap p-0 header">
    <a class="navbar-brand col-md-1 mr-0" href="/task/">戻る</a>
    <a  class="navbar-brand col-md-2 mr-0" href="{% url 'task:our_project_detail' project.0.pk %}"> プロジェクト詳細 </a>
    <a class="navbar-brand col-md-2 mr-0" href="#">タスク追加</a>
    <div class="btn-group">
        <div class="btn dropdown-toggle naviDrop" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            メンバー
        </div>
            <div class="dropdown-menu dropdown-menu-right" >
                <a class="dropdown-item" href="#"> {{ project.0.leader }} </a>
                {% if member != None %}
                    <div class="dropdown-divider"></div>
                    {% for person in member %}
                    <a class="dropdown-item" href="#"> {{ person }} </a>
                    {% endfor %}
                {% endif %}
            </div>
        </div>
    </div>
</nav>

{% if task == None %}
<div style="margin:20px"></div>
    <h4>タスクがありません</h4>
</div>
{% endif %}
{% endblock %}
```
CSSも追加する

```css:adjustment.css
.header {
    width: 99%;
    margin: 3px auto;
    box-shadow: 0 3px 5px rgba(95, 95, 95, 0.22);
}
```

### 画面確認
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/ce6f0cab-a923-b200-ac54-3f62807ea46f.png)

テストプロジェクトをクリックすると以下の画面が表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/caf6d5f1-01fe-c439-95c8-c54999e02446.png)

## プロジェクト詳細・更新・削除
プロジェクトの詳細画面と更新処理と削除処理を作る

### FORM・URL設定

更新フォームと削除フォームを作成する

```python:forms.py
""" プロジェクト更新 """
class ProjectUpdate(forms.ModelForm):

    class Meta:
        model = Project
        fields = (
            'name',
            'start_date', 'end_date',
            'details'
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
        self.fields['start_date'].widget.input_type = "date"
        self.fields['end_date'].widget.input_type="date"


""" プロジェクト削除 """
class ProjectDelete(forms.ModelForm):

    class Meta:
        model = Project
        fields = (
            'is_delete',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['is_delete'].widget.attrs['hidden'] = 'true'

```

URLの設定をする

```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'),  # 追加
]
```

```python:views.py
""" プロジェクトリーダ限定 """
class ProjectLeaderOnlyMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        is_return = True

        user = self.request.user

        l_result = Project.objects.filter(leader=user.use_cd, project_cd=self.kwargs["pk"])
        p_result = Project.objects.filter(project_cd=self.kwargs["pk"], is_delete=0)

        if len(l_result) == 0:
            is_return = False

        if len(p_result) == 0:
            is_return = False

        return is_return


""" プロジェクト情報 """
class ProjectDetail(ProjectUserOnlyMixin, generic.DetailView):
    model = Project
    template_name = 'task/project_detail.html'

    def get_context_data(self, **kwargs):
        user = self.request.user
        context = super().get_context_data(**kwargs)

        p_user = ProjectToUsers.objects.filter(project_cd=self.kwargs["pk"])
        project = Project.objects.filter(project_cd=self.kwargs["pk"], is_delete=0)

        context['member'] = p_user if len(p_user) > 0 else None

        context['project'] = project if len(project) > 0 else None

        return context


""" プロジェクト更新ページ """
class ProjectUpdate(ProjectUserOnlyMixin, generic.UpdateView):
    model = Project
    template_name = 'task/project_update.html'
    form_class = ProjectUpdate


    def get_success_url(self):
        return resolve_url('task:our_project_detail', pk=self.kwargs["pk"])


""" プロジェクト削除ページ """
class ProjectDelete(ProjectLeaderOnlyMixin, generic.UpdateView):
    model = Project
    template_name = 'task/project_delete.html'
    form_class = ProjectDelete

    def get_context_data(self, **kwargs):
        user = self.request.user
        context = super().get_context_data(**kwargs)

        p_user = ProjectToUsers.objects.filter(project_cd=self.kwargs["pk"])
        project = Project.objects.filter(project_cd=self.kwargs["pk"], is_delete=0)

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

        context['project'] = project if len(project) > 0 else None

        return context

    def get_initial(self): 
        return {'is_delete': True,}

    def get_success_url(self):
        return resolve_url('task:task_top')

```

アクセスしたユーザがリーダかを判定する'ProjectLeaderOnlyMixin'を作成し，削除系がリーダにしかできないようにする


### HTML作成
詳細ページ，更新ページ，削除ページそれぞれのHTMLを準備する

・詳細ページ

```html:project_detail.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} {{ project.0.name }} 詳細情報 {% endblock %}
{% block body %}
<div style="margin:20px">
    <table class="table">
        <tbody>
            <tr>
                <th>プロジェクト名</th>
                <td>{{ project.0.name }}</td>
            </tr>
            <tr>
                <th>開始日</th>
                <td>{{ project.0.start_date }}</td>
            </tr>
            <tr>
                <th>終了日</th>
                <td>{{ project.0.end_date }}</td>
            </tr>
            <tr>
                <th>詳細</th>
                <td>{{ project.0.details }}</td>
            </tr>
            <tr>
                <th>リーダ</th>
                <td>{{ project.0.leader }}</td>
            </tr>
            <tr>
                <th>メンバー</th>
                <td>
                    {% if member != None %}
                        <table class="no_border">
                            {% for perspn in member %}
                                <tr>
                                    <td>
                                        {{ perspn.user_cd }}
                                    </td>
                                </tr>
                            {% endfor %}
                        </table>
                    {% else %}
                        メンバーはいません
                    {% endif %}
                </td>
            </tr>
        </tbody>
    </table>

    <a class="btn btn-outline-secondary" href="{% url 'task:update_member' project.0.pk %}">メンバー更新</a>
    <hr>
    <a href="{% url 'task:our_project' project.0.pk %}" class="btn btn-primary" style="width: 120px; margin-right:10px;">戻る</a>
    <a href="{% url 'task:our_project_update' project.0.pk %}" class="btn btn-primary ">変更</a>
    {% if user == project.0.leader %}
        <a href="{% url 'task:our_project_delete' project.0.pk %}" class="delete-btn btn btn-primary">削除</a>
    {% endif %}
</div>
{% endblock %}
```

・更新ページ

```html:project_update.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} 更新 {% endblock %}
{% block body %}
<div style="margin:20px">
    <form action="" method="POST">
        {{ form.non_field_errors }}
        <table class="table">
            <tbody>
                {% for field in form %}
                    <tr>
                        <th><label for="{{ field.id_for_label }}">{{ field.label }}</label></th>
                        <td>{{ field }} <span style="color:red">{{ field.errors }}<span></td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
        {% csrf_token %}
        <button type="submit" class="btn btn-success" >更新</button>
        <a href="{% url 'task:our_project_detail' project.pk %}" class="btn btn-primary">戻る</a>
    </form>
</div>
{% endblock %}
```

・削除ページ

```html:project_delete.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} 削除 {% endblock %}
{% block body %}
<div style="margin:20px">

    <table class="table">
        <tbody>
            <tr>
                <th>プロジェクト名</th>
                <td>{{ project.0.name }}</td>
            </tr>
            <tr>
                <th>開始日</th>
                <td>{{ project.0.start_date }}</td>
            </tr>
            <tr>
                <th>終了日</th>
                <td>{{ project.0.end_date }}</td>
            </tr>
            <tr>
                <th>詳細</th>
                <td>{{ project.0.details }}</td>
            </tr>
            <tr>
                <th>リーダ</th>
                <td>{{ project.0.leader }}</td>
            </tr>
            {% if member != None %}
                <tr>
                    <th>メンバー</th>
                    <td>
                        <table class="no_border">
                            {% for person in member %}
                                <Tr>
                                    <td>{{ person.username }}</td>
                                </Tr>
                            {% endfor %}
                        </table>
                    </td>
                </tr>
            {% endif %}
        </tbody>
    </table>
    <br>

    <h2 style="color:rgba(255, 44, 44, 0.753);">本当に削除してよろしいですか？</h2>
    <br>

    <form action="" method="POST">
        {{ form.non_field_errors }}
                {% for field in form %}
                    {{ field }}
                {% endfor %}
        {% csrf_token %}
        <br>
        <a href="{% url 'task:our_project_detail' project.0.pk %}" class="btn btn-primary">戻る</a>
        <button type="submit" class="delete-btn btn btn-success">削除</button>
    </form>
{% endblock %}
```

CSSも追加する

```css:adjustment.css
.delete-btn{
    background-color: red;
    border: solid 1px black;
}

.delete-btn:hover{
    background-color: rgb(145, 0, 0);
}

.member_list td, .member_list th{
    padding:5px 10px;
    border-bottom: solid 1px black;
}

table.no_border, .no_border thead, .no_border tbody, .no_border tr, .no_border th, .no_border td{
    border: none;
}
```


### 画面確認
 ![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/6108a720-3143-6249-c845-421af1134b6a.png)
プロジェクト画面の`プロジェクト詳細`をクリックすると詳細画面が表示される

![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/0d063aa8-a738-4bd6-f67a-5db45b269578.png)
変更ボタンを押すと更新画面が表示されプロジェクトデータを更新することができる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/3c9d0a65-685e-ac8c-ffd0-9541abbe0942.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/b1a1f253-fe39-567a-a601-057e9bcbe120.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/b322c951-52cf-2d69-1cd0-ee6d12d35588.png)

削除ボタンを押すと確認画面が表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/4bf1d422-9b8b-0aca-e9fd-c9eee9d00eaa.png)

削除ボタンを押すと削除（論理削除）が行われる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/8bcd90ba-98d2-54b3-5dce-5c4b5e254644.png)


## メンバー管理
プロジェクトのメンバーを追加・削除が出来るようにする

### modelsの作成
ユーザとプロジェクトを結びつけるモデルを作成する

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

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

## メンバー追加・削除

### FORM・URL設定
メンバーを追加・削除する際のフォームの作成とURLの設定を行う

```python:forms.py
""" メンバー追加 """
class AddProjectMember(forms.ModelForm):

    class Meta:
        model = ProjectToUsers
        fields = (
            'project_cd',
            'user_cd'
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        where = []

        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
        self.fields['project_cd'].widget.attrs['hidden'] = 'true'

        leader = Project.objects.filter(project_cd=self.initial["project_cd"])
        in_member = ProjectToUsers.objects.filter(project_cd=self.initial["project_cd"])
        staff = User.objects.filter(is_staff=1)
        non_active = User.objects.filter(is_active=0)

        where.append(leader[0].leader_id)
        for in_men in in_member:
            where.append(in_men.user_cd_id) 

        for s in staff:
            where.append(s.pk)

        for non in non_active:
            where.append(non.pk)
        
        result = User.objects.exclude(use_cd__in=where)

        self.fields['user_cd'].queryset = result
```

`project_cd`は変更出来ないように非表示にして，追加するユーザもリーダや既にメンバーなどが追加されないようにリストから除外する。
また，`superuser`や`active`でないユーザも除外する
削除のフォームは作成しない

```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'),  # 追加
]
```

```python:views.py
""" プロジェクトメンバー追加 """

""" プロジェクトメンバー更新 """
class UpdateProjectMember(ProjectUserOnlyMixin, generic.CreateView):
    model = ProjectToUsers
    template_name = "task/update_member.html"
    form_class = AddProjectMember

    def get_context_data(self, **kwargs):
        user = self.request.user
        context = super().get_context_data(**kwargs)

        p_user = ProjectToUsers.objects.filter(project_cd=self.kwargs["pk"])
        project = Project.objects.filter(project_cd=self.kwargs["pk"], is_delete=0)

        context['member'] = p_user if len(p_user) > 0 else None

        context['project'] = project if len(project) > 0 else None

        return context

    def get_success_url(self):
        return resolve_url('task:update_member', pk=self.kwargs["pk"])

    def get_initial(self): 
        pc = self.kwargs["pk"]
        return {'project_cd': pc,}


""" プロジェクトメンバー削除 """
class ProjectDeleteMember(ProjectLeaderOnlyMixin, generic.DeleteView):
    model = ProjectToUsers

    def delete(self, *args, **kwargs):
        delete=1
        delete_ids = self.request.POST.getlist('delete_ids')
        if delete_ids:
            ProjectToUsers.objects.filter(id__in=delete_ids).delete()

        return redirect('task:update_member', pk=self.kwargs["pk"])
```

`UpdateProjectMember`クラスの`def get_initial(self)`で`project_cd`の初期値を設定している
`ProjectDeleteMember`クラスの`def delete(self, *args, **kwargs)`では選択されたものを削除する処理を行っている


### HTML作成
メンバーを追加，削除するページを作成する

```html:update_member.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} メンバー更新 {% endblock %}
{% block body %}

<div style="margin:50px 100px;">
    <form method="post" action="{% url 'task:our_project_delete_member' project.0.pk%}" >
        <table class="table">
            <tbody>
                <tr>
                    <th>リーダ</th>
                    <td>{{ project.0.leader }}</td>
                </tr>
                <tr>
                    <th>メンバー</th>
                    <td>
                        {% if member != None %}
                            <table class="no_border">
                                <thead class="MDC">
                                    <tr>
                                        <th>ユーザ名</th>
                                        <th>選択</th>
                                    </tr>
                                </thead>
                                {% for perspn in member %}
                                    <tr>
                                        <td>
                                            {{ perspn.user_cd }}
                                        </td>
                                        <td class="MDC">
                                            <input type="checkbox" class="CCB" name="delete_ids" value="{{ perspn.id }}">
                                        </td>
                                    </tr>
                                {% endfor %}
                            </table>
                        {% else %}
                            メンバーはいません
                        {% endif %}
                    </td>
                </tr>
            </tbody>
        </table>
        <a href="{% url 'task:our_project_detail' project.0.pk %}" class="btn btn-primary MD">戻る</a>
        <a class="btn btn-primary MD" style="color: white;" id="AMB">メンバー追加</a>
        {% if member != None and user == project.0.leader %}
            <a class="btn btn-primary MD MDB" style="color:white; margin-left: 10px;">メンバー退席</a>
            <a class="btn btn-primary MDC MDCB" style="color:white; margin-left: 10px;">キャンセル</a>
            <input class="delete-btn btn btn-primary MDC CPB" style="margin-left: 10px;" type="submit" value="退席">
            {% csrf_token %}
        {% endif %}
    </form>
</div>

<div id="modal-member">
    <form action="" method="POST">
        {{ form.non_field_errors }}
        {% for field in form %}
        <div class="form-group">
            {% if  field.label != "Project cd" %}
                <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
            {% endif %}
            {{ field }}
            <span style="color:red">{{ field.errors }}<span>
        </div>
        {% endfor %}
        {% csrf_token %}
        <button type="submit" class="btn btn-success btn-lg">追加</button>
        <a class="btn btn-primary btn-lg" style="color: white;" id="AMCB">戻る</a>
    </form>
</div>
{% endblock %}
```
プロジェクトのリーダの時だけメンバーを削除することができるようにする

### CSS・JavaScript作成
ユーザを追加・削除する際に画面に動きを付けているので，画面を制御するJavaScriptとCSSを設定する

```css:adjustment.css
.member_list td, .member_list th{
    padding:5px 10px;
    border-bottom: solid 1px black;
}

table.no_border, .no_border thead, .no_border tbody, .no_border tr, .no_border th, .no_border td{
    border: none;
}
```

```css:modal.css
 /* モーダル 背景 */
#modal-bg {
    display:none;
    width:100%;
    height:100%;
    background-color: rgba(0,0,0,0.5);
    position:fixed;
    top:0;
    left:0;
    z-index: 1;
}

/* モーダル コンテンツエリア */
#modal-member {
    display: none;
    width: 50%;
    height: 300px;
    padding:50px;
    border-radius:10px;
    position: absolute;
    left: 0px;
    right: 0px;
    top: 0px;
    bottom: 0px;
    margin: auto;
    background-color: white;
    position:fixed;
    z-index: 2;
}
```

```javascript:masta.js

// メンバー削除チェックボックス表示
$(function () {
    $('.MDC').hide();

    $('.MDB').click(function () {
        $('.MD').hide();
        $('.MDC').show();
    })
})

// メンバー削除チェックボックス非表示
$(function () {
    $('.MDCB').click(function () {
        $('.CCB').prop('checked', false);
        $('.CPB').css('visibility', 'hidden');
        $('.MDC').hide();
        $('.MD').show();
    })
})

// チェックボックスが選択されている場合のみボタンが活性状態になる
$(function() {
    $('.CPB').css('visibility', 'hidden');

    $('.CCB').change(function () {
        if ($('.CCB:checked').length == 0) {
            $('.CPB').css('visibility', 'hidden');
        } else {
            $('.CPB').css('visibility', 'visible');
        }
    })
})
```

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

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

新しく作成したcssとjsを`base.html`のヘッダに追加する

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

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

### 画面確認
今回はメンバー追加をするので新しくアカウントを作っておく

プロジェクト詳細情報ページの`メンバー更新`ボタンを押す
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/2269822b-0ccd-fd40-0d47-9c107111e29a.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/3222b7a2-601c-1ee0-1e18-f0ed189d739b.png)

`メンバー追加`ボタンを押し追加するメンバーを選択する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/7bf4a21c-10eb-8c2f-164b-c23cc98c6d9e.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/54d699db-98d2-0caa-d0cf-63289db572d4.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/822f5829-cd36-fd89-0df6-3ce169b2d10e.png)

`メンバー退席`ボタンを押しメンバーを選択する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/313f59c2-2a9d-821d-a313-1c7311b6b9a0.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/a3bc7914-4445-668e-a7fc-5d2269cc7e3f.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/5f1e403b-e220-68aa-fb05-c715b0acc0fd.png)

メンバーを追加・削除することができたのが確認できた

# 今回のまとめ
　プロジェクトの更新・削除は苦労することなく作ることができたが，追加できるメンバーの絞り込みや，メンバーの削除に手間取ってしまった。原因としては，データベースにアクセスする際の条件がうまく設定できなかったので，pythonの勉強がまだまだ必要だということが分かった。
 今回まででプロジェクトごとにユーザを限定出来るようになったので，次はプロジェクト内でタスクを作成し管理できるようにする

# シリーズ
 ・[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でタスク管理アプリをつくりたい！ プロジェクト管理編
・[Djangoでタスク管理アプリをつくりたい！ タスク管理編１](https://qiita.com/kuma3_it/items/7cb0f838fa4f99a74441)