LoginSignup
9
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-05

はじめに

前回(ユーザ情報編)で使用したプロジェクトを引き継いで作成する。

開発環境

・Windows10 professional 64bit
・Python 3.7.1
・Django 2.1.2
・MySQL 8.0.12

プロジェクト管理

ナビゲーションドロワーの作成

前準備

jQueryをつかうので,公式からダウンロードして,staticフォルダ内にjsフォルダを作りそこに格納する

下記のJavaScriptとCSSを作成する

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();
        }
    });

});
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を修正する

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

右上のTaskManを押すとナビゲーションドロワーが表示されるのを確認する
image.png

プロジェクトの作成

modelsの作成

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

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
    )

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

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を編集する

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

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

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'も編集する

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を作成する

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を追加する

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

タスクを選択すると以下の画面が表示される
image.png

新規プロジェクトの作成

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

FORM・URL設定

登録用のフォームとURLを作成する

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非対応)

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'),  # 追加
]
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_initialleaderの初期値を作成者にする

画面確認

image.png

プロジェクト一覧画面から作成ボタンを押す
image.png
image.png
入力して登録をすると一覧画面に表示される
image.png

プロジェクトページの作成

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

URL設定

ページのURLを設定する

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'),  # 追加
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を作成する

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も追加する

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

画面確認

image.png

テストプロジェクトをクリックすると以下の画面が表示される
image.png

プロジェクト詳細・更新・削除

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

FORM・URL設定

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

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の設定をする

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'),  # 追加
]
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を準備する

・詳細ページ

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 %}

・更新ページ

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 %}

・削除ページ

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も追加する

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
プロジェクト画面のプロジェクト詳細をクリックすると詳細画面が表示される

image.png
変更ボタンを押すと更新画面が表示されプロジェクトデータを更新することができる

image.png
image.png
image.png

削除ボタンを押すと確認画面が表示される
image.png

削除ボタンを押すと削除(論理削除)が行われる
image.png

メンバー管理

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

modelsの作成

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

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の設定を行う

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は変更出来ないように非表示にして,追加するユーザもリーダや既にメンバーなどが追加されないようにリストから除外する。
また,superuseractiveでないユーザも除外する
削除のフォームは作成しない

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

メンバーを追加,削除するページを作成する

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を設定する

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;
}
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;
}
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');
        }
    })
})
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のヘッダに追加する

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
image.png

メンバー追加ボタンを押し追加するメンバーを選択する
image.png
image.png
image.png

メンバー退席ボタンを押しメンバーを選択する
image.png
image.png
image.png

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

今回のまとめ

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

シリーズ

Djangoでタスク管理アプリをつくりたい! 環境構築編
Djangoでタスク管理アプリをつくりたい! ユーザ認証編
Djangoでタスク管理アプリをつくりたい! ユーザ情報編
・Djangoでタスク管理アプリをつくりたい! プロジェクト管理編
Djangoでタスク管理アプリをつくりたい! タスク管理編1

9
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
14