最新の投稿
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個位サンプルを入れてください。
djangoでblog その1
djangoでblogを作る方法
条件
マークダウンを使いたい
tailwindcssを使いたい
vueかreactを使いたい
なるべく簡単に作りたい
まず開発環境
1.ubuntu 22.04LTS or 24.04LTS //proxmox ve 8内で作業する為省略
2.pyenv pythonのバージョンを管理
3.nodenv nodejsのバージョンを管理
pyenv インストール
sudo apt update
sudo apt install build-essential libffi-dev libssl-dev zlib1g-dev liblzma-dev libbz2-dev \
libreadline-dev libsqlite3-dev libopencv-dev tk-dev git
gitでpyenv clone
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
echo '' >> ~/.bashrc
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init --path)"' >> ~/.bashrc
source ~/.bashrc
pyenv でインスール可能なバージョン
pyenv install --list
pyenv で 3.10.8をインストール
pyenv install 3.10.8
pyenv install 3.7.0
pyenv で現在インストールされているリスト
pyenv versions
pyenv の使うバージョンを全体で指定
pyenv global 3.10.8
pyenv フォルダ別にバージョン指定
cd /home/folder
pyenv local 3.7.0
nodenv インストール
pyenvの使い方によく似ています
git clone https://github.com/nodenv/nodenv.git ~/.nodenv
echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(nodenv init -)"' >> ~/.bashrc
nodejsバージョン一覧を取得用 node-build インストール
git clone https://github.com/nodenv/node-build.git $(nodenv root)/plugins/node-build
source ~/.bashrc
nodenvでインストールできる一覧
nodenv install --list
nodenv でインストール
nodenv install 18.16.1
nodenv install 16.14.0
nodenv で現在インストールされているリスト
nodenv versions
nodenv の使うバージョンを全体で指定
nodenv global 18.16.1
nodenv フォルダ別にバージョン指定
cd /home/folder
nodenv local 16.14.0
pyenv local 又は nodenv localを指定すると フォルダーに .python-version .node-versionが記録される
.python-versionの中身
3.10.8
.node-versionの中身
16.14.0
openssl で認証キーでプライベートサイトを作る
openssl で認証キーを作る場合CentOS6を使って作成
ubuntu14.04lts vinelinux などで作成しようとしたがエラー多発でうまくいかない。
参考サイト で作ったCAデレクトリーを運用中の/etc/nginxにコピー
nginxでのssl設定
server {
listen 443 ssl;
ssl on;
ssl_certificate /etc/nginx/CA/certs/cert.crt;
ssl_certificate_key /etc/nginx/CA/private/cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_client_certificate /etc/nginx/CA/cacert.pem;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
proxy_pass https://10.0.3.200;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /owncloud {
proxy_pass https://10.0.3.201/owncloud;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Meteorでコメントがあったり、BBSで投稿があった場合サイトの管理者(ここでは私)はその事を知りたいと思いました。
そこで、投稿があったらメールで管理者に知らせる為にメール送信機能を組み込みました。
参考サイト Meteorで、メール送信
いつものようにパッケージを組み込む
meteor add email
そして server/mailserver.jsへ自分が使っているsmtpサーバの情報を入力する。
これはGmailの設定例です。
Meteor.startup(function () {
process.env.MAIL_URL="smtp://メールアドレス:パスワード@smtp.gmail.com:587";
});
このサイトで使ってるBBSの例
client/template/bbs/bbs.html
<template name="bbscomment">
<span id="labeltitle">お名前<input type="text" id="bbs_name" value="{{cookie_bbs_name}}"/></span>
<span id="labeltitle">パスワード<input type="password" id="bbs_password" value="{{cookie_bbs_password}}"/></span>
<textarea id="bbscomment_message" class="form-control" rows="6" placeholder="パスワードを入力すると自分のコメントを編集できます。マークダウン使えますが、htmlタグ制限があります 。Aboutを見て下さい。"></textarea>
<span id="labeltitle">{{randomtext}}</span> 左の数値を入力して下さい <input type="text" id="random_id" />
<hr />
<button id="btn_comment" class="btn btn-info">追加</button>
</template>
client/template/bbs/bbs.js
Template.bbs.events({
'click #btn_comment': function(event, template){
var md5str;
var randomst = String($('#random_id').val());
if(randomst !== String(Session.get('bbs_random_text'))) return;
var bbs_name = $('#bbs_name').val();
var bbs_password = $('#bbs_password').val();
var bbs_comment = $('#bbscomment_message').val();
var mid = Session.get('bbs_mid');
// パスワードが空白でなければ、 Crypt を使って Base64で暗号化
if(bbs_password !==''){
md5str = CryptoJS.MD5(bbs_password).toString(CryptoJS.enc.Base64);
} else {
md5str ='';
}
if(bbs_name==='' || bbs_comment==='') return;
//コメントのインサート
Meteor.call('bbsCommentInsert', mid, bbs_name, md5str, bbs_comment,
function(err, result){
if(!err){ // エラーがなければ
$('#random_id').val('');
$('#bbs_name').val('');
$('#bbs_password').val('');
$('#bbscomment_message').val('');
if(bbs_password !== '') {
// クッキーをサイトルートで一年間セット
Cookie.set('_cookie_bbs_name', bbs_name, {
path: '/',
expires: 365
});
Cookie.set('_cookie_bbs_password', bbs_password, {
path: '/',
expires: 365
});
};
// メールの送信はここから
var maintitle =NewMainBbs.findOne({mid:Session.get('bbs_mid')}).maintitle;
var to = 'kiyo@yosiki.org';
var from = 'kiyo@yosiki.org';
var subject = 'kiyoSite からメール';
var mes = maintitle +' '+bbs_name+ '様からコメントがありました。';
// Meteor server methodsのsendEmailをコール
Meteor.call('sendEmail', to, from, subject, mes,
function (err, result) {
if (err) {
console.log('メールの送信に失敗');
} else {
console.log('メール送信 完了');
}
});
server/server.js
Meteor.methods({
//メールの送信
'sendEmail': function(to, from, subject, text){
check([to, from, subject, text], [String]);
// 同じクライアントからの、ほかのメソッド呼び出しが
// メール送信完了を待つことなく実行が開始されるように
// 設定します。
this.unblock();
Email.send({
to: to,
from: from,
subject: subject,
text: text
});
}
});
起動スクリプトを調べていたら、 Upstart を使ってお手軽 daemon 化と言う方法があるようだ。
ルール
/etc/init/ に拡張子 .confにして保存する。
description "(説明を書く)"
author "(あなたの名前) <(メアド@ドメイン)>"
start on runlevel [(Run Level)]
stop on runlevel [(Run Level)]
expect fork (デーモンの場合は必要)
chdir (作業ディレクトリ)
exec (起動コマンド)
respawn (実行中のジョブが異常終了すると再実行するよう指定)
SysVinit(従来の)方法に比べて簡単で起動が早いらしい。
ubuntuでは Upstart方式に変えていく方針らしい。
実験してみた。
#kiyo-site nginx port:xxxx deamon
description "kiyo-site --- meteor deamon"
start on runlevel [2345]
stop on runlevel [!2345]
expect fork
respawn
chdir /usr/lib/bundle_kiyo
exec sh /usr/lib/bundle_kiyo/start.sh
$ sudo initctl reload-configuration
再起動