Django ベースの図書管理システム構築:認証フローとカスタムページネーションの設計

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 %}

タグ: Django pagination user-authentication MySQL Bootstrap

8月26日 06:06 投稿