Django プロジェクトの初期設定として、データベース接続と認証モデルのカスタマイズを行います。ここでは MySQL を使用し、標準の User モデルを拡張するアプローチを採用します。
# settings.py 内の主要設定項目
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'library_core',
'HOST': 'localhost',
'PORT': 3306,
'USER': 'admin_root',
'PASSWORD': 'secure_pwd_123',
'OPTIONS': {
'charset': 'utf8mb4'
}
}
}
# 静的ファイルの管理設定
STATIC_URL = '/assets/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static_files')]
# カスタムユーザーモデルの指定(重要)
AUTH_USER_MODEL = 'main_system.Personnel'
データ構造の定義 (Models)
図書のメタデータと関係性を保持するためのモデルクラスを作成します。AbstractUser を継承することで、セキュリティ関連フィールドを自動生成しつつ、プロジェクト固有の属性を追加可能です。
# main_system/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class Personnel(AbstractUser):
"""従業員・管理者情報の拡張モデル"""
contact_phone = models.BigIntegerField(verbose_name='連絡先番号', null=True, blank=True)
bio_description = models.TextField(verbose_name='人物説明', null=True, blank=True)
def __str__(self):
return f"{self.username} ({self.contact_phone})"
class Literature(models.Model):
"""図書目録データ"""
catalog_id = models.AutoField(primary_key=True)
title_name = models.CharField(max_length=50, verbose_name='書籍タイトル')
cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='価格')
release_date = models.DateTimeField(auto_now_add=True, verbose_name='公開日')
# 外鍵関係(出版社)
publisher_info = models.ForeignKey('Publisher', on_delete=models.CASCADE, related_name='publications')
# 多対多関係(著者)
writer_list = models.ManyToManyField('Writer', related_name='works')
def __str__(self):
return self.title_name
class Publisher(models.Model):
"""出版元情報"""
company_name = models.CharField(max_length=50, unique=True)
head_office = models.CharField(max_length=100)
def __str__(self):
return self.company_name
class Writer(models.Model):
"""執筆者情報"""
name = models.CharField(max_length=50)
birth_year = models.IntegerField(null=True)
# ワンツーワン関係(詳細情報)
profile_detail = models.OneToOneField('ProfileDetail', on_delete=models.SET_NULL, null=True, related_name='author_profile')
def __str__(self):
return self.name
class ProfileDetail(models.Model):
"""執筆者の詳細プロフィール"""
personal_email = models.EmailField()
residence_area = models.CharField(max_length=100)
URL 経路の配置 (Urls)
アプリケーション間のルーティングを整理し、機能ごとに適切なエンドポイントを割り当てます。
# main_project/urls.py (ルート)
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('system/', include('main_system.urls')),
]
# main_system/urls.py (アプリ内)
from . import controllers
urlpatterns = [
path('dashboard/', controllers.show_dashboard, name='dash_view'),
path('catalog/list/', controllers.render_catalog_list, name='list_books'),
path('auth/sign_up/', controllers.handle_registration, name='reg_user'),
path('auth/sign_in/', controllers.perform_login, name='log_in'),
path('auth/change_pass/', controllers.update_password, name='ch_pass'),
path('auth/sign_out/', controllers.end_session, name='log_out'),
path('catalog/create/', controllers.add_new_book, name='add_book'),
path('catalog/update//', controllers.modify_book, name='edit_book'),
path('catalog/remove//', controllers.delete_book, name='del_book'),
]
処理ロジックの実装 (Views)
リクエスト処理において、セッション管理とデータ検証を適切に行うことが重要です。特に認証が必要なページにはdecorator を適用して保護します。
登録とログイン処理
# main_system/controllers.py
from django.shortcuts import render, redirect, HttpResponse
from django.contrib import auth
from .models import Personnel, Literature, Publisher, Writer
def handle_registration(req):
if req.method == 'POST':
uname = req.POST.get('user_name')
passwd = req.POST.get('secret_code')
check_passwd = req.POST.get('verify_code')
if passwd != check_passwd:
return HttpResponse("パスワードに不一致があります")
if Personnel.objects.filter(username=uname).exists():
return HttpResponse("该 ID は既に登録済みです")
Personnel.objects.create_user(username=uname, password=passwd)
return render(req, 'auth/signup_template.html')
return render(req, 'auth/signup_template.html')
def perform_login(req):
if req.method == 'POST':
uname = req.POST.get('user_name')
passwd = req.POST.get('secret_code')
account = auth.authenticate(request=req, username=uname, password=passwd)
if account:
auth.login(req, account)
next_url = req.GET.get('next') or 'dash_view'
# セッション有効期限の設定(例:30 日間)
req.session.set_expiry(2592000)
return redirect(next_url)
else:
return HttpResponse("認証に失敗しました")
return render(req, 'auth/login_template.html')
図書のリスト表示とページネーション
大量のデータを扱う際、DB に負荷をかけずに効率的に表示するためにページネーションを実装します。ここでは、後述するクラスベースのパaginator を利用した実装を示します。
from django.contrib.auth.decorators import login_required
from .utils.pager_tool import PagerManager
@login_required(login_url='/system/auth/sign_in/')
def render_catalog_list(req):
target_page = req.GET.get('page', 1)
total_items = Literature.objects.count()
# ページネーションクラスの活用
pager = PagerManager(target_page=target_page, total_records=total_items, items_per_page=5)
# DB クエリのスライス処理
display_set = Literature.objects.all()[pager.start_idx : pager.end_idx]
context = {
'book_list': display_set,
'pager_html': pager.generate_nav(),
'is_active_page': pager.current
}
return render(req, 'catalog/catalog_list.html', context)
テンプレート階層構造
共通要素(ヘッダー、フッター、ナビゲーション)を Base テンプレートに集約し、各画面で Block 制御を行うことで保守性を高めています。
<!-- base_layout.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>図書管理ポータル</title>
<link rel="stylesheet" href="{% static 'css/main_style.css' %}">
{% load static %}
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<a class="navbar-brand" href="#">BMS Core</a>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="{% url 'dash_view' %}">ホーム</a></li>
<li><a href="{% url 'list_books' %}">図書一覧</a></li>
</ul>
<div class="navbar-right">
{% if request.user.is_authenticated %}
<span class="navbar-text">Welcome, {{ request.user.username }}</span>
<a href="{% url 'log_out' %}" class="btn btn-danger btn-xs">Logout</a>
{% else %}
<a href="{% url 'reg_user' %}">Register</a>
<a href="{% url 'log_in' %}">Login</a>
{% endif %}
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-md-2 sidebar">
<!-- サイドメニュー -->
<div class="list-group">
<a href="#" class="list-group-item active">メインメニュー</a>
<a href="#" class="list-group-item">カテゴリ別検索</a>
<a href="#" class="list-group-item">新規登録</a>
</div>
</div>
<div class="col-md-10 content-area">
{% block body_content %}
<!-- 各画面のコンテンツ -->
{% endblock %}
</div>
</div>
</div>
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
{% block footer_script %}{% endblock %}
</body>
</html>
カスタムページネーションロジック
手動での HTML 構築は重複が多くバグが起きやすいため、汎用的なクラスに統合します。現在のページに基づき、前後のボタンや総ページ数に応じたラベルを表示制御します。
# utils/pager_tool.py
class PagerManager:
def __init__(self, current_page, total_records, items_per_page=5, max_visible_pages=11):
try:
self.current = int(current_page)
except ValueError:
self.current = 1
if self.current < 1:
self.current = 1
self.total = total_records
self.per_page = items_per_page
self.limit = max_visible_pages
self._calc_bounds()
def _calc_bounds(self):
self.start_idx = (self.current - 1) * self.per_page
self.end_idx = self.current * self.per_page
pages_total, remainder = divmod(self.total, self.per_page)
if remainder:
pages_total += 1
self.pages_total = pages_total
half_limit = (self.limit - 1) // 2
if self.pages_total <= self.limit:
self.page_start = 1
self.page_end = self.pages_total + 1
elif self.current <= half_limit:
self.page_start = 1
self.page_end = self.limit + 1
elif (self.current + half_limit) > self.pages_total:
self.page_end = self.pages_total + 1
self.page_start = self.pages_total - self.limit + 1
else:
self.page_start = self.current - half_limit
self.page_end = self.current + half_limit + 1
def generate_nav(self):
html_parts = ['<nav aria-label="pager">,<ul class="pagination" role="navigation">']
# 初め戻るボタンの処理は省略のため簡略化
for i in range(self.page_start, self.page_end):
state = 'class="active"' if i == self.current else ''
link = f'{state}<a href="?page={i}">{i}</a>'
html_parts.append(f'<li>{link}</li>')
html_parts.extend(['</ul>', '</nav>'])
return ''.join(html_parts)
編集フォームのテンプレート
既存データの反映のために、SELECT タグなどで選択済みの状態(selected)を動的にセットする必要があります。
{% extends 'base_layout.html' %}
{% block body_content %}
<div class="panel panel-default">
<div class="panel-heading">図書情報編集</div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
<p>
<label>書名</label>
<input type="text" name="title" value="{{ item.title }}" class="form-control">
</p>
<p>
<label>販売価格</label>
<input type="number" step="0.01" name="cost" value="{{ item.cost_price }}" class="form-control">
</p>
<p>
<label>出版社選択</label>
<select name="publish_id" class="form-control">
{% for pub in publishers %}
{% if item.publisher_info.id == pub.id %}
<option value="{{ pub.id }}" selected>{{ pub.company_name }}</option>
{% else %}
<option value="{{ pub.id }}">{{ pub.company_name }}</option>
{% endif %}
{% endfor %}
</select>
</p>
<p>
<label>著者登録(複数可)</label>
<select multiple name="writer_ids" class="form-control">
{% for writer in authors %}
{% if writer in item.writer_list.all %}
<option value="{{ writer.id }}" selected>{{ writer.name }}</option>
{% else %}
<option value="{{ writer.id }}">{{ writer.name }}</option>
{% endif %}
{% endfor %}
</select>
</p>
<button type="submit" class="btn btn-primary">保存更新</button>
</form>
</div>
</div>
{% endblock %}