django-blog-2
djangoでBlog その2
今回(djangoでBlog その1の続き)はdjango-breezeを使って
djnago + inertia.js + react + tailwind.css + viteをubuntu22.04LTSに簡単に構築する方法
完成するとこんな感じです
django-breezeを使えるようにする
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=True やauto_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個位サンプルを入れてください。