django-blog-2

カテゴリー:django 作成日:2024年6月13日12:29

djangoでBlog その2

今回(djangoでBlog その1の続き)はdjango-breezeを使って

djnago + inertia.js + react + tailwind.css + viteをubuntu22.04LTSに簡単に構築する方法

完成するとこんな感じです

django-breezeを使えるようにする

django-breeze-githubサイト

mkdir djb-react && cd djb-react
pyenv local 3.8.10

nodenv local 18.16.1

python -m venv venv
source venv/bin/activate

pipを最新版にする
(venv) pip install --upgrade pip
(venv) pip install django-breeze

(venv) django-breeze startproject conf . //「.」を忘れないように

(venv) django-breeze startapp app


react 又は vue3が選択可能のようです

今回はreactを選択

(venv) django-breeze create-app react 
vueを選択する場合は
(venv) django-breeze create-app vue3
とします

conf/settings.pyに以下設定

INSTALLED_APPS = [
  #..............
  'django_breeze',
  'app'
  #..............
]

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_TZ = True

# axios 設定追加 今回はaxiosは使わないが一応設定しとく

CSRF_HEADER_NAME = 'HTTP_X_XSRF_TOKEN'
CSRF_COOKIE_NAME = 'XSRF-TOKEN'


npm install も忘れずに行ってください

ここまででほぼ設定まで完了しています


src/main.jsx ここにはdjangoとreactをinetertia.jsを使って連結する内容が書かれています。

src/index.cssにはtailwind cssが書かれています。

@tailwind base;
@tailwind components;
@tailwind utilities;



ただreactでimportする時src以下を@/で置き換える為 vite.config.jsに以下を追加

resolve: {
    resolve: {
      extensions: [".js", ".jsx", ".json"],
    },
  // ここから
    alias: {
      '@': resolve(__dirname, './src')
    },
 // ここまで追加
},


/src/Layout/MyLayoutを
import MyLayout from '@/Layout/MyLayout';
とできます。
階層が深くなった時にも '../../MyLayout'いや'../../../MyLayout'だったかな?
と悩まずにすみます。

後tailwindは多機能な感じで良いのですが少し楽をしたいのでflowbite flowbite-reactを入れます

npm install flowbite flowbite-react

tailwind.config.jsをflowbite対応に書き換えます

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
    'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}',
    'node_modules/flowbite/**/*.{js,jsx,ts,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [
    /** forms, **/
    require('flowbite/plugin'),
  ],
};

それとreactでiconを使いたいので lucide-react 日付を日本語で表示したいので momentをインストール

npm install moment lucide-react

djangoには marshmallowをインストール
databaseをjsonへ変換したりvalidation機能もあります

(venv)pip install marshmallow



1.データを作ります app/models.py

from django.db import models
from django.core import validators
from django.utils.timezone import now

class Item(models.Model):

    SEX_CHOICES = (
        (1, '男性'),
        (2, '女性'),
    )

    name = models.CharField(
        verbose_name='名前',
        max_length=200,
    )
    age = models.IntegerField(
        verbose_name='年齢',
        validators=[validators.MinValueValidator(1)],
        blank=True
    )
    sex = models.IntegerField(
        verbose_name='性別',
        choices=SEX_CHOICES,
        default=1
    )
    memo = models.TextField(
        verbose_name='備考',
        max_length=300,
        blank=True
    )
    created_at = models.DateTimeField(
        verbose_name='登録日',
        default=now
    )

    # 管理サイト上の表示設定
    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'アイテム'
        verbose_name_plural = 'アイテム'

ここで気をつける事は

from django.utils.timezone import now

created_at = models.DateTimeField(
       verbose_name='登録日',
        default=now
)



登録日は now関数を使う事です auto_now_add=Trueauto_now=True にすると後で日付を書き換える事ができないからです。

2. djangoに登録します adminのユーザとパスワードも作成します

(venv)python manage.py makemigrations
(Venv)python manage.py migrate

## admin User, Passwordを作る
(venv)python manage.py createsuperuser エンターキーを叩くとUser,Email,passwordを聞いてきます。
例:
User :demo
Email: demo@email.com
Password: password!!11AA
Password確認: password!!11AA
Passwordは先頭がアルファベットで大文字小文字を各1個以上数値も含ませ8文字以上との制約があります。 

3.conf/urls.pyを修正します

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("app.urls")),
]

4.app/urls.pyを作成して編集します

from django.urls import path

from . import views

app_name = 'app'

urlpatterns = [
    path("", views.index, name="index"),
    path("create/", views.create, name="create"),
    path("store/", views.store, name="store"),
    path("edit/<int:id>", views.edit, name="edit"),
    path("update/<int:id>", views.update, name="update"),
    path("delete/<int:id>", views.delete, name="delete"),
    path("show/<int:id>", views.show, name="show"),
]

5.続いて app/views.pyを編集していきます

from django.shortcuts import redirect
from inertia import render, share
from .models import Item
from .serializers import ItemSchema  # <-serializers.pyこのあと作ります
from marshmallow import ValidationError

def index(request):
  obj = Item.objects.order_by('-created_at')
  # auth_bool = request.user.is_superuser
  return render(request, 'Items/Index', props={
    'items': obj,
  })

def create(request):
  return render(request, 'Items/Create', {})

def show(request, id):
    obj = Item.objects.get(id=id)
    return render(request, 'Items/Show', props={'item': obj})

def store(request):
  if request.method == 'POST':
    try:
      schema = ItemSchema()
      data = schema.loads(request.body)
      obj = Item.objects.create(**data)
    except ValidationError as err:
      share(request, error="Exists errors on form")
      share(request, error=err.messages)
    else:
      share(request, success=f"Item {obj.name} created")
      return redirect("app:index")

def edit(request, id):
  obj = Item.objects.get(id=id)
  return render(request, 'Items/Edit', props={'item': obj})

def update(request, id):
  obj = Item.objects.get(id=id)
  schema = ItemSchema()
  param = schema.loads(request.body)
  obj.name = param["name"]
  obj.age = param["age"]
  obj.sex = param["sex"]
  obj.memo = param["memo"]
  obj.save()
  #obj.objects.filter(id=id).update(**data)
  share(request, success=f"Item {obj.name} update")
  return redirect("app:index")

def delete(request, id):
  obj = Item.objects.get(id=id)
  obj.delete()
  share(request, success="Item Deleted")
  return redirect("app:index")

6. app/serializers.pyを作成し編集します

from marshmallow import Schema, fields, validate

class ItemSchema(Schema):
    id = fields.Int()
    name = fields.Str(validate=validate.Length(min=1))
    age = fields.Int()
    sex = fields.Int()
    memo = fields.Str()
    created_at = fields.DateTime()



7. src/pagesにItemsフォルダーをつくりItems/Index.jsxを作ります

mkdir -p src/pages/Items
touch src/pages/Items/Index.jsx
touch src/pages/Items/Create.jsx
touch src/pages/Items/Edit.jsx
touch src/pages/Items/Show.jsx



8. /src/pages/Items/Index.jsxを編集します

import { useState, useEffect } from 'react';
import { router, Link } from '@inertiajs/react';
import MyLayout from '@/Layout/MyLayout';
import moment from 'moment';


export default function Index({items}){

    const [prePage] = useState(6)

    const [totalPage] = useState(items.length)

    const [currentPage, setCurrentPage] = useState(1)

    const nextPage = () => {
        if (currentPage !== Math.ceil(items.length / prePage)) {
            setCurrentPage(currentPage+1)
        }
    }

    const prevPage = () => {
        if(currentPage !== 1){
            setCurrentPage(currentPage-1)
        }
    }

    const gotoPage = (page) => {
        setCurrentPage(page)
    }

    const [lcount] = useState(Math.ceil(totalPage / prePage))

    const pageNumbers = [];
    for (let i=1; i<Math.ceil(totalPage/prePage)+1; i++){
        pageNumbers.push(i);
    }

    function deletePost( id ) {
        if (confirm(`No.${id}を削除してよろしいですか`)) {
            router.delete(`/delete/${id}`);
        }
    }

    function dateFormat(data) {
        return  moment(data).format('YYYY年MM月DD日 HH:mm')
    }

    function get_sex_display(sex) { return (sex === 1) ? '男性' : '女性' }

    return (
        <>
        <MyLayout>
            <div className="flex flex-wrap columns-2 md:columns-3 lg:columns-4">
            { items.slice((currentPage -1) * prePage, currentPage * prePage).map( (item) => (
            <div key={item.id}   className="mt-4 max-w-sm rounded border-gray-400 overflow-hidden shadow-xl">
                <div className="p-4 w-72">
                <h2 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
                    <p>{item.name}</p>
                </h2>
                <p  className="font-normal text-gray-700 dark:text-gray-400">
                        年齢 {item.age} 歳
                </p>
                <p className="font-normal text-gray-700 dark:text-gray-400">
                    性別 { get_sex_display(item.sex) }
                </p>
                <p className="font-normal text-gray-700 dark:text-gray-400">
                    備考 { item.memo }
                </p>
                <p className="font-normal text-gray-700 dark:text-gray-400">
                    登録日 {dateFormat(item.created_at)}
                </p>
                <div className="px-6 pt-4 pb-2">
                    <Link href={`/show/${item.id}`} className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
                            確認
                    </Link>
                    <Link href={`/edit/${item.id}`} className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
                            編集
                    </Link>
                    <button onClick={() => deletePost(item.id)} className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-red-700 mr-2 mb-2">
                            削除
                    </button>
               </div>
            </div>
            </div>
            ))}
            </div>
            <br />
            <nav aria-label="Page navigation example">
            <ul className="flex items-center  -space-x-px h-8 text-sm">
                <li className="flex">
                  { currentPage > 1 ? (
                     <button onClick={prevPage} className="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
                      <span className="sr-only">Previous</span>
                      <svg className="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
                     </svg>
                     </button>
                       ) : (<p></p>)
                    }
                </li>
                <li className="flex">
                        {pageNumbers.map((number) => (
                            <button onClick={() => gotoPage(number)}
                            className={
                                currentPage === number
                                ? "z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
                                : "flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
                            }
                        >{number}</button>
                        ))}
                </li>
                <li className="flex">
                    { currentPage < lcount ? (
                    <button onClick={nextPage} className="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
                        <span className="sr-only">Next</span>
                        <svg className="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
                        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
                        </svg>
                    </button>
                        ) : (<p></p>)
                    }
                </li>
            </ul>
            </nav>
        </MyLayout>
        </>
    )
}

9. src/Layout/MyLayout.jsx作成編集

import React from "react";
import { Navigation, Header } from './index';


export default function MyLayout({children}) {
    return  (
        <>
        <div className="flex flex-col h-screen bg-gray-100">
            <Header />
            <div className="flex flex-grow bg-gray-100">
              <div className="w-2/12">
                <Navigation />
              </div>
              <div className="w-10/12 m-2 bg-gray-100 overflow-y-auto">
               {children}
              </div>
            </div>
        </div>
        </>
    )
}

10. src/Layout/Header.jsx, src/Layout/Navigation.jsx src/Layout/index.jsx作成
src/Layout/Header.jsx

import { Link } from '@inertiajs/react';
import {headerMenus} from '../icons';

export const Header = () => {
    return (
        <div className="h-12 bg-gray-100 text-yellow-900 flex items-center mx-4 pl-3 px-4">
        <ul className="mx4">
            {headerMenus.map((menu, index) => (
                <li className="mb-1 group" key={index}>
                    <Link href={menu.link} className="flex font-semibold items-center py-1 px-4 text-gray-900 hover:bg-red-500 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
                        <menu.icon  className="mr-1 size-[24px] text-indigo-500"></menu.icon>{menu.label}
                    </Link>
                </li>
            ))}
        </ul>
        </div>
    );
}

src/Layout/Navigation.jsx

import { Link } from '@inertiajs/react';
import { naviMenus } from '../icons';

export const Navigation = () =>{
    return (
        <nav className="bg-gray-100 mx-auto p-4 text-black-600 flex flex-col items-center">
        <ul className="mx4">
            {naviMenus.map((menu, index) => (
                <li className="mb-1 group" key={index}>
                    <Link href={menu.link} className="flex font-semibold items-center py-1 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
                        <menu.icon  className="mr-1 size-[24px] text-green-300"></menu.icon>{menu.label}
                    </Link>
                </li>
            ))}
        </ul>
        </nav>
    );
};

src/Layout/index.jsx

export * from './Header';
export * from './Navigation';
export * from './MyLayout';

11. Icon関係のファイルを作成します

mkdir src/icons
touch src/icons/hraderMenus.jsx
touch src/icons/navMenus.jsx
touch src/icons/index.jsx

src/icons/headerMenus.jsx

import {Home} from 'lucide-react';

export const headerMenus = [
    {
        link: '/',
        label: 'ホーム',
        icon: Home,
    },
];

src/icons/naviMenus.jsx

import {Home, Plus} from 'lucide-react';

export const naviMenus = [
    {
        link: '/',
        label: 'ホーム',
        icon: Home,
    },
    {
        link: '/create',
        label: '新規',
        icon: Plus,
    },
]

src/icons/index.jsx

export * from './headerMenus';
export * from './naviMenus';

12. app/admin.py編集します

from django.contrib import admin

# Register your models here.
from .models import Item


@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):

    class Meta:
        verbose_name = 'ユーザ'
        verbose_name_plural = 'ユーザ'

vscode ターミナルで npm run dev
もう一つターミナルを開いて python manage.py runserver
127.0.0.1:8000/adminで10個位サンプルを入れてください。