【Django入門】DjangoとVue.jsで作ったカレンダーに祝日入力機能追加してみた!

この記事は約17分で読めます。
広告

前の記事でVue CLIとDjango REST frameworkで作ったカレンダーはGETしかできませんでした。POST、DELETEができるようにして祝日の登録削除機能を追加しました。
完成品
使い方
Let’s check next holyday!をクリクックすると次の祝日までの日にちが分かる。
祝日登録削除画面にいくと祝日名、日付を入力して登録すると祝日が登録される。
祝日一覧のidを削除idに入れて削除を押すと祝日が削除できます。
ご自由にいじってください。適当な名前で祝日登録したり削除しても大丈夫です。

元々は、こちらの記事で作ったカレンダーに祝日を書き込めるようにしました。なのでこちらの記事を読んだ前提で書いた記事です。

広告

Django側修正

Restframeworkではroutersとviewsetsのクラスによりアクセス先とデータモデルを結びつけているようです。(下の参照のところ見るとgenericsはroutersはいらなそうです。前回作ったものもいりませんでした。)

前回作ったものはrest_frameworkはgenerics.ListAPIViewを使いましたが、これはGETのみしか操作できません。参照https://qiita.com/murapon/items/8a6a1715712ddd1a4ac1
genericsの中にPOSTできるものもあります。

しかし今回はgenericsではなく以下で利用したviewsets.ModelViewSetを使っていきます。GET(データ読み出し)、POST(データ登録)、DELETE(データ削除)が使えます。
イメージ的にはGET、POST、DELETEをここで受け止めて、モデルに読み出し、書き込み、削除する感じでしょうか。viewsets.ModelViewSetは下の記事で一度アプリを作ってるため流用しやすいので使いました。

generics.ListAPIViewをviewsets.ModelViewSetに変えます。新しくapp_cal/viewsets.pyを作ります。継承するクラスを変更するだけです。

from rest_framework import viewsets
from .models import Holiday
from .serializers import HolidaySerializer

class HolidayList(viewsets.ModelViewSet):
    queryset = Holiday.objects.all()
    serializer_class = HolidaySerializer

app_cal/views.pyは今回使わなくなりますが、エラーになるので修正しておきます。app_cal/url.pyはそのままですが同じく使ってません。

# from rest_framework import generics
# from django.views import generic
# from .models import Holiday
# from .serializers import HolidaySerializer


# class HolidayList(generics.ListAPIView):
#     queryset = Holiday.objects.all()
#     serializer_class = HolidaySerializer
    
from django.shortcuts import render
from django.views import View

class HajimeteView(View):
    def get(self, request, *args, **kwargs):
       return render(request,"app_cal/index.html")

project_cal/urls.py とrouters.pyでアクセス先を変更します。
まずpath(‘app_cal’, TemplateView.as_view(template_name=’app_cal/index.html’)),で
app_calにアクセスしたらtemplatesのinde.htmlを表示するようにします。

GET、POST、Deleteのアクセス先は次の部分で設定されています。
urls.pyのpath(‘api/’, include(router.urls)),
routers.pyのrouter.register(‘app_cal’,HolidayList)

設定したところの動きを見ます。
api/にアクセスすると→include(router.urls)によりrouterのurlsを見にいく。
routerのurlsではrouters.pyのrouter.register(‘app_cal’,HolidayList)によりurlsが’app_cal’となります。さらに’app_cal’はModelViewSetのHolidayListと紐づいています。
なのでapi/app_calに対してGET、POST、Deleteを行えばデータベースの操作ができます。
project_cal/routers.pyを追加します。

from rest_framework import routers
from app_cal.viewsets import HolidayList


router = routers.DefaultRouter()
router.register('app_cal',HolidayList)

project_cal/urls.pyを修正します。

from django.contrib import admin
from django.urls import path, include
from .routers import router# 追加
from django.views.generic import TemplateView # 追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),# 追加
    path('app_cal', TemplateView.as_view(template_name='app_cal/index.html')), # 追加

    # path('calendar/', include('app_cal.urls')),
    # path('',include("app_cal.urls")),
]
補足ですが、このように違うviewsetに振り分けて使うこともできました。なのでアプリで振り分けたりできます。
router = routers.DefaultRouter()
router.register('app_cal',HolidayList)
router.register('article', ArticleViewSet)

目次へ

Vue側

POST、DELETEの追加

ナビとして残しておいたhomeを押したら祝日登録削除画面へ遷移するようにいます。
祝日登録削除画面はhomeを押したときに表示するsrc/components/HelloWorld.vueを修正して使います。

<template>
  <div class="hello">
      <div class="container">
    <h1>祝日一覧</h1>
    
<table class="table">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">名前</th>|
            <th scope="col">日付</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(name_date, index) in res_name_date" :key=index>
            <th scope="row">{{name_date.id}}</th>
            <td>{{name_date.name}}</td>|
            <td>{{name_date.date}}</td>
          </tr>
        </tbody>
      </table>
<br>
<div id="app">
    <div class="title">
      <p>祝日名: (new_name)</p>
      <input v-model="new_name">
    </div>
    <div class="day">
      <p>日付: (new_date)</p>
      <input v-model="new_date">
    </div>
      <button v-on:click="createNewUser">投稿</button>
    <div class="del">  
      <p>削除id:</p>
      <input v-model="del_id">
    </div>
      <button v-on:click="deleteid">削除</button>
  </div></div>
  </div>
</template>

<script>
import axios from 'axios';
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
 data() {
        return {
            new_name:'',
            new_date:'',
            del_id:'',
            res_name_date: [],
            loading: false,

           newArticle: {'name': null, 'date': null },
        };
    },
    mounted() {
        axios.defaults.xsrfCookieName = 'csrftoken' ;// ←ココと
        axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"; // ←ココに追加しました
         this.getArticles();
    },
    methods: {
         getArticles() {
            
            this.loading = true;
            axios.get('https://localhost-人それぞれ違うサーバー名-1.paiza-user-free.cloud:8000/api/app_cal/?format=json')
                .then(response => {
                    this.res_name_date=response.data
                  })
          .catch((err) => {
           this.loading = false;
           console.log(err);
          })
         },
        createNewUser: function(){
             axios.post('https://localhost-人それぞれ違うサーバー名-1.paiza-user-free.cloud:8000/api/app_cal/',{
                 "name": this.new_name,
                 "date": this.new_date
         
                 })
                  .then(location='/app_cal')
                  .catch(error => console.log(error))     
        },
        deleteid: function(){
            var delurl="https://localhost-人それぞれ違うサーバー名.paiza-user-free.cloud:8000/api/app_cal/"+this.del_id+"/";
             axios.delete(delurl)
                  .then(location='/app_cal')
                  .catch(error => console.log(error))     
        }            
     },
}

</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.table{
        margin: auto;
    }
    .container{
    text-align:center;
}

#app{
    text-align:center;
}
.title,.day,.del{
    width:80%;
    margin:0 auto;
    padding:15px 0;
    margin-bottom:30px;
    background-color:whitesmoke;
}
input{
    padding:5px 10px;
    min-width:250px;
}
button{
    width:80%;
    background-color: teal;
    color:white;
    padding:20px 0;
    border:none;
    border-radius:10px;
    cursor:pointer;
    margin-bottom: 50px;
}
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>
Axiosを使っています。
CSRF対策でクッキーを付ける必要があるときは以下のように()の最後に追加してください。
GETの場合
axios.get(省略,{withCredentials: true})

fetchで使っているところ
components/Calendar.vue
    // 祝日データ取得
    this.$http(this.$httpHoliday)
→this.$http(this.$httpHoliday,{credentials:'include'})

Django側のCORS対策と合わせて変更前の記事を参照してください。https://kikuichige.com/13438/
最終的にHerokuへデプロイしてるものはCors、CSRFが働くようにDjango側のdjango-cors-headersやVue側のcredentialsは削除してます。
逆にこの辺を入れてエラーになるのはVue側とDjango側が別々のオリジン(例えば:8000と:8080)のとき。POSTするとプリフライトがうまくいかずエラーになります。
src/components/Calendar_test.vueはAxiosではなくfetchのままでいじりません。
// 祝日データ取得
    this.$http(this.$httpHoliday)

上記はsrc/main.js で以下のように定義されていますので必要であれば修正してください。
// fetchを定義
app.config.globalProperties.$http = (url, opts) => fetch(url, opts)
// DRFのURL(API用)
app.config.globalProperties.$httpHoliday = 'https://localhost-人それぞれ違うサーバー名.paiza-user-free.cloud:8000/api/app_cal/?format=json'

目次へ

Router部分の修正

src/App.vueが以下のままだと
<router-link to="/">Calendar(表示しないときクリック)</router-link>
アクセスした際にプラウザのアドレスバーがhttps://略/となります。これで再読み込みするとエラーになります。今表示しているカレンダーの画面はDjango側で'/app_cal'に設定されているため'/'ではDjango側でpathが設定されていないため表示されません。なのでDjango側と合わせます。

という具合にVueのrouterはアドレスバーに表示させてはいるものの、そのアドレスにアクセスするとサーバー側で対応してないとエラーになるので注意が必要です。
祝日登録・削除の"/app_cal/home"は今回Django側で対策してないので、そのまま再読み込みするとエラーになります。
祝日登録して完了したとき/app_cal/homeを再読み込みする処理だとエラーになります。なので今回はHelloWorld.vueで対策。POST、DELETEのあとは.then(location='/app_cal')によりカレンダー表示画面に遷移するようにしました。
src/App.vueの変更部分
  <nav>
    <router-link to="/app_cal/home">祝日登録・削除</router-link> |
    <router-link to="/app_cal">Calendar(表示しないときクリック)</router-link>
  </nav>

src/router/index.js の変更部分
const routes = [
  {
    path: '/app_cal/home',
    name: 'home',
    component: HomeView
  },

    {
    path: '/app_cal',
    name: 'django-calender',
    components: {
      default: Calendar,
    }
  }
]

その他、祝日までの日数を求める処理を修正しました。

calcNextHoliday() {
        let mintermDays = 365;
        let minholiday = null;
      // 次の祝日までの日数、および次の祝日のオブジェクトを返す
      for (const holiday in this.holidayList) {
        const holidayToDate = new Date(this.holidayList[holiday].date.split('-'))
        const todayToDate = new Date(this.today.split('-'))
        const termDays = this.calcTermDays(todayToDate, holidayToDate)
        if(termDays < mintermDays && termDays >=0){
            minholiday = holiday;
            mintermDays = termDays;
        }
      }

あとがき

今回Cors、CSRFについては触れていませんが、Restframework+Vueで結構はまるポイントです。この辺は私の別の記事で詳しく検討していますのでご覧ください。

目次へ
この記事を書いたイチゲを応援する
Vプリカでのお支払いがおすすめです。

MENTAやってます(ichige)

コメント

タイトルとURLをコピーしました