Python | Django | ECサイトの作成方法

2021年9月14日

公開日:2021/9/14
更新日:2021/9/29

Pythonには,DjangoというWebアプリケーションフレームワークがある.フレームワークのため,Djangoを利用するとWebアプリを通常よりも短時間で開発することが可能になる.

2021/8/25に「class-based viewsにおけるログインの実装方法」を作成した.当該記事で実装したプロジェクトをそのまま引き継いだ上で,本記事では,「ECサイトの作成方法」を以下6項目にて記す.

  1. 準備
  2. 商品登録サイトの作成
    (1) 基本サイトの作成
    (2) 絞り込み機能の追加
    (3) 並び替え機能の追加
    (4) 詳細画面への遷移機能の追加
  3. 商品をカートに追加実装
    (1) 基本のカート実装
    (2) 精算画面の実装
  4. 住所情報の追加実装
  5. 原子性を含むデータベースの実装
  6. 反省点
  7. 参照

◆実施環境

Python 3.8.8
Django 3.2.3

■ECサイトの作成方法

  1. 準備

2021/8/25に作成した「class-based viewsにおけるログインの実装方法」で利用した"myclassbasedviewslogintest"を開く.VS Codeの"File"をクリックし,"Open Folder"から当該フォルダを選ぶ.

“myclassbasedviewslogintest"を選択すると,以下のように表示される.

上記フォルダ中のプロジェクト名"views_login"を"ec_site"に変更する.変更後,以下のようになる.

プロジェクト名"views_login"を"ec_site"に変更したので,以下フォルダ内の修正を行う."ec_site/manage.py"を以下のように編集する.

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ec_site.settings') # 変更箇所
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

“ec_site/asgi.py"を以下のように編集する.

"""
ASGI config for ec_site project. # 変更箇所

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ec_site.settings') # 変更箇所

application = get_asgi_application()

“ec_site/wsgi.py"を以下のように編集する.

"""
WSGI config for ec_site project. # 変更箇所

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ec_site.settings') # 変更箇所

application = get_wsgi_application()

“ec_site/settings.py"を以下のように編集する.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'ec_site.urls' # 変更箇所

~省略~

WSGI_APPLICATION = 'ec_site.wsgi.application' # 変更箇所

“account/views.py"を以下のように編集する.

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, FormView
from django.views.generic.base import TemplateView, View
from .forms import RegistrationForm, LoginForm
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView

class TopView(TemplateView):
    template_name = 'top.html'

class RegistrationView(CreateView):
    template_name = 'registration.html'
    form_class = RegistrationForm

class MyLoginView(LoginView):
    template_name = 'login.html'
    authentication_form = LoginForm

    def form_valid(self, form):
        remember = form.cleaned_data['remember']
        if remember:
            self.request.session.set_expiry(1000000)
        return super().form_valid(form)

class MyLogoutView(LogoutView):
    pass

class MyUserView(TemplateView):
    template_name = 'user.html'

    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)
  1. 商品登録サイトの作成

(1) 基本サイトの作成

VS Codeの"View"を開き,"Terminal"をクリックする.VS Codeの下部にターミナルが開くので,”conda activate 仮想環境名”を実行し,仮想環境に移行する(移行方法の詳細はこちら).ターミナルで"cd ec_site"(プロジェクト名)を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py startapp chem_store”を実行する.実行後,以下のように"chem_store"が作成される.

“chem_store/models.py"を以下のように編集する.

from django.db import models

class ChemicalTypes(models.Model): # ProductTypes
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model): # Products
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model): # ProductPictures
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)

“ec_site/settings.py"の"INSTALLED_APPS"を以下のように編集する.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account',
    'chem_store', # 追記箇所
]

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py makemigrations chem_store”を実行する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py makemigrations chem_store

Migrations for 'chem_store': # 以下出力箇所
  chem_store\migrations\0001_initial.py
    - Create model ChemicalTypes
    - Create model Manufacturers
    - Create model Chemicals
    - Create model ChemicalPictures

続けて,ターミナルに"python manage.py migrate chem_store"を入力する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py migrate chem_store

Operations to perform: # 以下出力箇所
  Apply all migrations: chem_store
Running migrations:
  Applying chem_store.0001_initial... OK

VS Codeにおいて,SQLiteをインストールし(詳細はこちら参照),"SQLITE EXPLORER"を開くと,各テーブルを以下赤枠に確認できる.

“chem_store/admin.py"を以下のように編集する.

from django.contrib import admin
from .models import (
    ChemicalTypes, Manufacturers, Chemicals,
    ChemicalPictures,
)

admin.site.register(
    [ChemicalTypes, Manufacturers, Chemicals, ChemicalPictures]
)

“ec_site/settings.py"の最下部に追記し,以下のように編集する.なお,"LOGOUT_REDIRECT_URL"の下部に記載されていた"SESSION_COOKIE_AGE"は削除した.

LOGIN_URL = '/account/login'
LOGIN_REDIRECT_URL = '/account/top'
LOGOUT_REDIRECT_URL = '/account/login'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 以下追記箇所
MEDIA_URL = '/media/'

“ec_site/urls.py"に追記し,以下のように編集する.

from django.contrib import admin
from django.urls import path, include
from . import settings # 追記箇所(3~4行目)
from django.contrib.staticfiles.urls import static

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

if settings.DEBUG: # 追記箇所(11~12行目)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

“ec_site"プロジェクトフォルダに以下のように"media"フォルダを作成する.

“chem_store/models.py"の"class ChemicalPictures"にて"chemical_pictures/"をアップロード先にしたので,"media"フォルダに"chemical_pictures"フォルダを以下のように作成する.

“account/models.py"に追記し,以下のように編集する.

from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser, PermissionsMixin,
)
from django.urls import reverse_lazy

class UserManager(BaseUserManager):
    def create_user(self, email, date_of_birth, password=None):
        if not email:
            raise ValueError('You need Email address!')
        user = self.model(
            email=email,
            date_of_birth=date_of_birth,
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, date_of_birth, password=None): # 追記箇所(19~29行目)
        user = self.model(
            email=email,
            date_of_birth=date_of_birth,
        )
        user.set_password(password)
        user.is_staff = True
        user.is_active = True
        user.is_superuser = True
        user.save(using=self._db)
        return user

class MyUser(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=100)
    email = models.EmailField(max_length=100, unique=True)
    date_of_birth = models.DateField()
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['date_of_birth']

    objects = UserManager()

    def get_absolute_url(self):
        return reverse_lazy('account:top')

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py createsuperuser”を実行する.以下のように出力されるので,適当に記述する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py createsuperuser

Email: admin@email.com # 以下出力箇所(記述箇所)
Date of birth: 1999-01-01
Password: 
Password (again):
Superuser created successfully.

続けて,ターミナルで"python manage.py runserver"を入力すると以下が出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 05, 2021 - 17:33:01
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/admin"を入力すると以下画面に遷移する.

上記で作成したユーザーの"email"と"password"を入力し,"Log in"をクリックする.

“Log in"後,以下画面に遷移する.作成したクラスである"Chemical pictures", “Chemical Types", “Chemicals", “Manufacturers"が赤枠のように追加となった.

上記赤枠の"Manufacturerss"をクリックすると以下画面に遷移する.赤枠の"ADD MANUFACTURERS"をクリックする.

以下画面に遷移するので,製造者を3社登録する.

登録後,以下赤枠のように製造者が表示される.次は,"Chemical Typess"をクリックする.

以下赤枠の"ADD CHEMICAL TYPES"をクリックする.

以下画面に遷移するので,化学物質のタイプを登録していく.今回は,米国法規(TSCA; Toxic Substances Control Act)に基づく化学物質のタイプ(Class 1, Class 2, Polymer)を登録する.

Class 1: 構造の確定した化合物
Class 2: ポリマー以外の反応生成物
Polymer: ポリマー

登録後,以下赤枠のように各種化学物質のタイプが表示される.次は,"Chemicalss"をクリックする.

赤枠の"ADD CHEMICALS"をクリックする.

以下画面に遷移するので,化学物質を登録していく.

参考として,最初の物質は以下のように登録した.

登録後,以下赤枠のように各種化学物質が表示される.次は"Chemical Picturess"をクリックする.

赤枠の"ADD CHEMICAL PICTURES"をクリックする.

以下画面に遷移するので,化学物質の画像を登録していく.

参考として,最初の物質は以下のように登録した.

登録後,以下赤枠のように各種化学物質の画像が表示される.

“chem_store"に"urls.py"を以下のように作成する.

“chem_store/views.py"を以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

“chem_store/urls.py"を以下のように編集する.

from django.urls import path
from .views import (
    ChemicalListView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
]

“ec_site/urls.py"に追記し,以下のように編集する.

from django.contrib import admin
from django.urls import path, include
from . import settings
from django.contrib.staticfiles.urls import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('account/', include('account.urls')),
    path('chem_store/', include('chem_store.urls')), # 追記箇所
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

“ec_site/templates/base.html"に追記し,以下のように編集する.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>
  <body>
    <nav class="navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'account:top' %}">&nbsp;Top</a>
      {% if user.is_authenticated %}
      <a class="navbar-brand" href="{% url 'account:logout' %}">Logout</a>
      <a class="navbar-brand" href="{% url 'chem_store:chemical_list' %}">Chemical List</a> # 追記箇所
      {% else %}
      <a class="navbar-brand" href="{% url 'account:login' %}">Login</a>
      <a class="navbar-brand" href="{% url 'account:registration' %}">Registration</a>
      {% endif %}
      <a class="navbar-brand" href="{% url 'account:user' %}">User Page</a>
    </nav>
    {% block content %}{% endblock %}
  </body>
</html>

“ec_site/templates"に"chem_store"フォルダを作成し,その中に,"chemical_list.html"を以下のように作成する.

“ec_site/templates/chem_store/chemical_list.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 06, 2021 - 18:29:28
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Logout"をクリックする.

以下画面に遷移するので,"Email Address"と"Password"を記入し,"Login"ボタンをクリックする.

今回は以下情報でログインした.

ログイン後,以下画面に遷移する."Chemical List"をクリックする.

以下画面に遷移する.登録した化学物質の一覧を確認できる.

(2) 絞り込み機能の追加

“ec_site/templates/chem_store/chemical_list.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}"> # 追記箇所(5~8行目)
  <p>Chemical Type: <input type="text" name="chemical_type_name"/></p>
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

“chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self): # 以下追記箇所
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        return query

上記を保存し,ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 07, 2021 - 14:36:23
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Chemical List"をクリックする.

以下画面に遷移する.赤枠に"Chemical Type"の絞り込み機能が実装されている.

赤枠のウィンドウに"Class 1″と記入をして,"Perform"をクリックする.

クリック後,以下のように"Class 1″のTypeが絞り込みされた.

赤枠のウィンドウに"Polymer"と記入をして,"Perform"をクリックする.

クリック後,以下のように"Polymer"のTypeが絞り込みされた.その後,空白のまま"Perform"ボタンをクリックする.

以下のように絞り込み前の画面に戻る.

絞り込み後,ウィンドウに記述した種類が消えてしまうので,以下では記述した種類が消えないように実装する.

“chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        return query

    def get_context_data(self, **kwargs): # 以下追記箇所
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        return context

“ec_site/templates/chem_store/chemical_list.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}">
  <p>Chemical Type: <input type="text" name="chemical_type_name" value="{{ chemical_type_name }}"/></p> # 変更箇所
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/account/top"に遷移する."Chemical List"をクリックする.

以下画面に遷移する.

赤枠のウィンドウに"Class 1″を記述し,"Perform"ボタンをクリックする.

以下のように"Class 1″の"Chemical Type"を絞り込むことができた.加えて,記述した"Class 1″もウィンドウに残り続けることができた.

絞り込みの対象が"Chemical Type"のみなので,以下では対象を増やすように実装する."ec_site/templates/chem_store/chemical_list.html"に追加し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}">
  <p>Chemical Type: <input type="text" name="chemical_type_name" value="{{ chemical_type_name }}"/></p>
  <p>Chemical Name: <input type="text" name="chemical_name" value="{{ chemical_name }}"/></p> # 追記箇所(7~8行目)
  <p>CAS No.: <input type="text" name="cas_name" value="{{ cas_name }}"/></p>
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None) # 追記箇所(17~18行目)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name: # 追記箇所(23~30行目)
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '') # 追記箇所(26~27行目)
        context['cas_name'] = self.request.GET.get('cas_name', '')
        return context

上記を保存し,ブラウザから"http://127.0.0.1:8000/account/top"に遷移する."Chemical List"をクリックする.

以下画面に遷移する.絞り込みのウィンドウが以下赤枠のように増えた.

“Chemical Name"のウィンドウに"Methanol"と記述し,絞り込みを行う.

以下のように"Methanol"を絞り込むことができた.

“CAS No."のウィンドウに"68955-20-4″と記述し,絞り込みを行う.

以下のように"68955-20-4″を絞り込むことができた.

(3) 並び替え機能の追加

以下では並び替えが対象を増やすように実装する."ec_site/templates/chem_store/chemical_list.html"に追加し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}">
  <p>Chemical Type: <input type="text" name="chemical_type_name" value="{{ chemical_type_name }}"/></p>
  <p>Chemical Name: <input type="text" name="chemical_name" value="{{ chemical_name }}"/></p>
  <p>CAS No.: <input type="text" name="cas_name" value="{{ cas_name }}"/></p>
  <p>Order by price: # 追記箇所(9~12行目)
    ascend(昇順)<input type="radio" name="order_by_price" value="1">
    descend(降順)<input type="radio" name="order_by_price" value="2">
  </p>
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

“ec_site/chem_store/views.py"を追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0) # 追記箇所(31~35行目)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        return context

上記を保存し,ブラウザから"http://127.0.0.1:8000/account/top"に遷移する."Chemical List"をクリックする.

以下画面に遷移する.赤枠のように値段で並び替えができるような機能が実装された.

赤枠の"descend(降順)"にチェックを入れ,"Perform"ボタンをクリックすると,Price(値段)が降順になった.ただ,その際,チェックも消えた.

並び替え後もチェックが残るように以下にて実装する."ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0) # 追記箇所(43~47行目)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

“templates/chem_store/chemical_list.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}">
  <p>Chemical Type: <input type="text" name="chemical_type_name" value="{{ chemical_type_name }}"/></p>
  <p>Chemical Name: <input type="text" name="chemical_name" value="{{ chemical_name }}"/></p>
  <p>CAS No.: <input type="text" name="cas_name" value="{{ cas_name }}"/></p>
  <p>Order by price:
    ascend(昇順)<input type="radio" name="order_by_price" value="1" {% if ascending %}checked{% endif %}> # 変更箇所(10~11行目)
    descend(降順)<input type="radio" name="order_by_price" value="2" {% if descending %}checked{% endif %}>
  </p>
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td>{{ chemical.name }}</td>
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に遷移する.以下画面に遷移する.

赤枠の"descend(降順)"にチェックを入れ,"Perform"ボタンをクリックすると,Price(値段)が降順になった.その際,チェックも残る.

(4) 詳細画面への遷移機能の追加

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView # 追記箇所
from django.contrib.auth.mixins import LoginRequiredMixin

import os
from .models import(
    Chemicals,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView): # 以下追記箇所
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'), # 追記箇所
]

“ec_site/chem_store/views.py"に"chemical_detail.html"を記述しているので,"templates/chem_store"に"chemical_detail.html"を以下のように作成する.

“templates/chem_store/chemical_detail.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      <img width="200px" height="200px" src={{ picture.picture.url }}>
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
  </div>
</div>
{% endblock %}

“templates/chem_store/chemical_list.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
<h2>chemical list</h2>
<form method="get" action="{% url 'chem_store:chemical_list' %}">
  <p>Chemical Type: <input type="text" name="chemical_type_name" value="{{ chemical_type_name }}"/></p>
  <p>Chemical Name: <input type="text" name="chemical_name" value="{{ chemical_name }}"/></p>
  <p>CAS No.: <input type="text" name="cas_name" value="{{ cas_name }}"/></p>
  <p>Order by price:
    ascend(昇順)<input type="radio" name="order_by_price" value="1" {% if ascending %}checked{% endif %}>
    descend(降順)<input type="radio" name="order_by_price" value="2" {% if descending %}checked{% endif %}>
  </p>
  <p><input type="submit" value="Perform"/></p>
</form>
<table class="table table-striped table-bordered">
<thread>
  <tr>
    <td>Type</td>
    <td>Chemical name</td>
    <td>CAS No.</td>
    <td>Price</td>
    <td>Stock amount</td>
    <td>Manufacturer</td>
  </tr>
</thread>
<tbody>
{% for chemical in object_list %}
  <tr>
    <td>{{ chemical.chemical_type.name }}</td>
    <td><a href="{% url 'chem_store:chemical_detail' pk=chemical.id %}">{{ chemical.name }}</td> # 変更箇所
    <td>{{ chemical.cas }}</td>
    <td>{{ chemical.price }} Yen</td>
    <td>{{ chemical.stock }} Amounts</td>
    <td>{{ chemical.manufacturer.name }}</td>
  </tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動する.以下赤枠のように化学物質名にリンクが実装された.

上記の"Chemical list"の画面で"Methanol"をクリックすると,以下詳細画面に遷移する.

写真の表示方法を変更するため,以下に実装する."ec_site/templates/chem_store/chemical_detail.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0"> # 編集箇所(4~12行目)
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
  </div>
</div>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動する."Methanol"をクリックする.

以下詳細画面に遷移する.最初の画像サイズのみ"200px*200px"だが,2番目以降の画像サイズは"50px*50px"に変更され表示された.

  1. 商品をカートに追加実装

(1) 基本のカート実装

“ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser # 追記箇所

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model): # 以下追記箇所
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py makemigrations chem_store”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py makemigrations chem_store

Migrations for 'chem_store': # 以下出力箇所
  chem_store\migrations\0002_cartitems_carts.py
    - Create model Carts
    - Create model CartItems

続けて,”python manage.py migrate chem_store”を実行する.以下が出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py migrate chem_store

Operations to perform: # 以下出力箇所
  Apply all migrations: chem_store
Running migrations:
  Applying chem_store.0002_cartitems_carts... OK

“ec_site/templates/chem_store/chemical_detail.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
    {% if object.stock %} # 追記箇所(18~21行目)
      <input type="number" id="quantity" name="quantity" min="1" max="{{ object.stock }}"><br>
      <button id="add_chemical" type="button" class="btn btn-primary">add to cart</button>
    {% endif %}
  </div>
</div>
{% endblock %}

上記を保存し,上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動する."Methanol"をクリックする.

以下のように詳細画面に遷移する.赤枠が実装された.

実際にカートの商品を追加する処理を実装するため,"ec_site/templates/chem_store/chemical_detail.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
    {% if object.stock %}
      <input type="number" id="quantity" name="quantity" min="1" max="{{ object.stock }}"><br>
      <button id="add_chemical" type="button" class="btn btn-primary">add to cart</button>
    {% endif %}
  </div>
</div>
<script> # 追記箇所(24~42行目)
$('#add_chemical').click(function(){
  var quantity = $("#quantity").val();
  $.ajax({
    url: "{% url 'chem_store:add_chemical' %}",
    type: "POST",
    data: {chemical_id: "{{ object.id }}", quantity},
    dataType: "json",
    success: function(json){
      if(json.message){
        alert(json.message);
      }
    },
    error: function(error){
      alert(error.responseJSON.message);
    }
  });
});
</script>
{% endblock %}

jQueryをインポートするため,以下URLにアクセスする.

https://developers.google.com/speed/libraries

上記URLにアクセスすると以下画面に遷移するので,青く反転させた箇所をコピーする.

“ec_site/templates/base.html"にコピーした内容を以下のように貼り付ける.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> # 追記箇所
  </head>
  <body>
    <nav class="navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'account:top' %}">&nbsp;Top</a>
      {% if user.is_authenticated %}
      <a class="navbar-brand" href="{% url 'account:logout' %}">Logout</a>
      <a class="navbar-brand" href="{% url 'chem_store:chemical_list' %}">Chemical List</a>
      {% else %}
      <a class="navbar-brand" href="{% url 'account:login' %}">Login</a>
      <a class="navbar-brand" href="{% url 'account:registration' %}">Registration</a>
      {% endif %}
      <a class="navbar-brand" href="{% url 'account:user' %}">User Page</a>
    </nav>
    {% block content %}{% endblock %}
  </body>
</html>

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404 # 追記箇所
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required # 追記箇所
from django.http import JsonResponse, Http404

import os
from .models import( # 追記箇所
    Chemicals, Carts, CartItems
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

@login_required # 以下追記箇所
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

“ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager): # 追記箇所(63~67行目)

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager() # 追記箇所

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
    add_chemical,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'), # 追記箇所
]

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
September 09, 2021 - 15:33:15
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http;//127.0.0.1:8000″をクリックするとブラウザが立ち上がる.ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動する.以下画面に移動するので,"Methanol"をクリックする.

以下画面である"Methanol"の詳細ページに遷移する.

以下赤枠のように数字を入力し,"add to cart"をクリックする.

ターミナルには以下のように"CSRF token missing or incorrect"が出力される.

[09/Sep/2021 15:38:38] 
"GET /chem_store/chemical_detail/1 HTTP/1.1" 200 2195
Forbidden (CSRF token missing or incorrect.): 
/chem_store/add_chemical/

[09/Sep/2021 15:43:57] 
"POST /chem_store/add_chemical/ HTTP/1.1" 403 2519

“CSRF token missing or incorrect"が出力されたので,"ec_site/templates/chem_store/chemical_detail.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
    {% if object.stock %}
      <input type="number" id="quantity" name="quantity" min="1" max="{{ object.stock }}"><br>
      <button id="add_chemical" type="button" class="btn btn-primary">add to cart</button>
    {% endif %}
    <input name="csrfToken" value="{{ csrf_token }}" type="hidden"> # 追記箇所
  </div>
</div>
<script>
$('#add_chemical').click(function(){
  var quantity = $("#quantity").val();
  var token = $('input[name="csrfToken"]').attr('value'); # 追記箇所(23~33行目)
  $.ajaxSetup({
    beforeSend: function(xhr){
      xhr.setRequestHeader('X-CSRFToken', token);
    }
  })
  $.ajax({
    url: "{% url 'chem_store:add_chemical' %}",
    type: "POST",
    data: {chemical_id: "{{ object.id }}", quantity},
    dataType: "json",
    success: function(json){
      if(json.message){
        alert(json.message);
      }
    },
    error: function(error){
      alert(error.responseJSON.message);
    }
  });
});
</script>
{% endblock %}

ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動し,"Methanol"をクリックする.以下画面(“http://127.0.0.1:8000/chem_store/chemical_detail/1")に遷移する.

以下赤枠のように数量"2″を記述し,"add to cart"をクリックすると,赤枠のメッセージが出現する.

VS Codeにおいて,SQLiteをインストールし(詳細はこちら参照),"SQLITE EXPLORER"を開くと,"carts"テーブルや"cart_items"テーブルを以下のように確認できる.

“carts"テーブルを右クリックし,"Show Table"をクリックすると以下内容を確認できる.

“cart_items"テーブルを右クリックし,"Show Table"をクリックすると以下内容を確認できる.上記でカートに入れた数量をデータベースに格納することができた.

上記の"Methanol"の詳細ページにて,追加で赤枠のように数量を入力して"add to cart"をクリックしてもメッセージは表示されない.

ターミナルには以下のように"UNIQUE constraint failed"が出力され,データベースには追加されない.

return Database.Cursor.execute(self, query, params)
django.db.utils.IntegrityError: 
UNIQUE constraint failed: 
cart_items.chemical_id, cart_items.cart_id

すでに追加があることが分かるような実装を以下で行う."ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404

import os
from .models import(
    Chemicals, Carts, CartItems
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs): # 追記箇所(57~63行目)
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

“ec_site/templates/chem_store/chemical_detail.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
    {% if object.stock %}
      {% if is_added %} # 追記箇所(19~24行目)
        <p class="btn btn-danger">added already to cart</p>
      {% else %}
        <input type="number" id="quantity" name="quantity" min="1" max="{{ object.stock }}"><br>
        <button id="add_chemical" type="button" class="btn btn-primary">add to cart</button>
      {% endif %}
    {% endif %}
    <input name="csrfToken" value="{{ csrf_token }}" type="hidden">
  </div>
</div>
<script>
$('#add_chemical').click(function(){
  var quantity = $("#quantity").val();
  var token = $('input[name="csrfToken"]').attr('value');
  $.ajaxSetup({
    beforeSend: function(xhr){
      xhr.setRequestHeader('X-CSRFToken', token);
    }
  })
  $.ajax({
    url: "{% url 'chem_store:add_chemical' %}",
    type: "POST",
    data: {chemical_id: "{{ object.id }}", quantity},
    dataType: "json",
    success: function(json){
      if(json.message){
        alert(json.message);
      }
    },
    error: function(error){
      alert(error.responseJSON.message);
    }
  });
});
</script>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_detail/1″に移動する.すでにカートに追加しているので,以下赤枠のようにカートに追加済みであることを意味する"added to cart already"が表示される.

カートに商品を追加した後,処理が確実になされないように,"ec_site/templates/chem_store/chemical_detail.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-6 offset-3">
  <div style="float: left; padding: 0 20px 20px 0">
    {% for picture in object.chemicalpictures_set.all %}
      {% if forloop.first %}
        <div><img width="200px" height="200px" src={{ picture.picture.url }}></div>
      {% else %}
        <div><img width="50px" height="50px" src={{ picture.picture.url }}></div>
      {% endif %}
    {% endfor %}
  </div>
  <div>
    <p>Chemical Name: {{ object.name }}</p>
    <p>CAS No.: {{ object.cas }}</p>
    <p>Price: {{ object.price }}</p>
    <p>Stock Amounts: {{ object.stock }}</p>
    {% if object.stock %}
      {% if is_added %}
        <p class="btn btn-danger">added to cart already</p>
      {% else %}
        <input type="number" id="quantity" name="quantity" min="1" max="{{ object.stock }}"><br>
        <button id="add_chemical" type="button" class="btn btn-primary">add to cart</button>
      {% endif %}
    {% endif %}
    <input name="csrfToken" value="{{ csrf_token }}" type="hidden">
  </div>
</div>
<script>
$('#add_chemical').click(function(){
  var quantity = $("#quantity").val();
  var token = $('input[name="csrfToken"]').attr('value');
  $.ajaxSetup({
    beforeSend: function(xhr){
      xhr.setRequestHeader('X-CSRFToken', token);
    }
  })
  $.ajax({
    url: "{% url 'chem_store:add_chemical' %}",
    type: "POST",
    data: {chemical_id: "{{ object.id }}", quantity},
    dataType: "json",
    success: function(json){
      if(json.message){
        $('#add_chemical').attr('class', 'btn btn-danger'); # 追記箇所(45~47行目)
        $('#add_chemical').html('added to cart already!');
        $('#add_chemical').prop('disabled', true);
        alert(json.message);
      }
    },
    error: function(error){
      alert(error.responseJSON.message);
    }
  });
});
</script>
{% endblock %}

上記を保存し,ブラウザから"http://127.0.0.1:8000/chem_store/chemical_list"に移動する.移動後,赤枠の化学物質をクリックする.

以下化学物質の詳細ページに遷移する.

“Stock Amounts"が"3″だが,"4″にして"add to cart"をクリックすると,赤枠のメッセージ(exceed stock amounts)が出現する.

ありえない数字である"-1″にして"add to cart"をクリックすると,赤枠のメッセージ(enter amount more than zero)が出現する.

“Stock Amount"の"3″を超過しない"2″にして"add to cart"をクリックすると,赤枠のメッセージ(added item to cart)が出現する.メッセージの"OK"をクリックする.

以下のように"added to cart already"が表示される.

“SQLITE EXPLORER"を開き,"carts"テーブルを右クリックして"Show Table"をクリックすると以下が確認できる."user_id"は同じなので,変化なし.

“cart_items"テーブルを右クリックして"Show Table"をクリックすると以下が確認できる."id"欄の"2″に今回追加した内容を以下のように確認できる.

(2) 精算画面の実装

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView # 追記箇所

import os
from .models import(
    Chemicals, Carts, CartItems
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0: 
            response = JsonResponse({'message': 'enter amount more than zero!'}) 
            response.status_code = 403 
            return response 
        cart = Carts.objects.get_or_create( 
            user=request.user 
        ) 
        if all([chemical_id, cart, quantity]): 
            CartItems.objects.save_item( 
               quantity=quantity, chemical_id=chemical_id, 
               cart=cart[0] 
            ) 
            return JsonResponse({'message': 'added item to cart!'}) 

class CartItemsView(LoginRequiredMixin, TemplateView): # 以下追記箇所 
    template_name = os.path.join('chem_store', 'cart_items.html') 

    def get_context_data(self, **kwargs): 
        context = super().get_context_data(**kwargs) 
        user_id = self.request.user.id 
        query = CartItems.objects.filter(cart_id=user_id) 
        total_price = 0 
        items = [] 
        for item in query.all(): 
            total_price += item.quantity * item.chemical.price 
            picture = item.chemical.chemicalpictures_set.first() 
            picture = picture.picture if picture else None 
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
    add_chemical, CartItemsView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'), # 追記箇所
]

“ec_site/chem_store/views.py"で"cart_items.html"を記載したので,"ec_site/templates/chem_store"フォルダに"cart_items.html"を以下のように作成する.

“ec_site/templates/chem_store/cart_items.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
{% if items %}
<table>
<tbody>
{% for item in items %}
  <tr>
    <td style="width:20%">
    {% if item.picture %}
    <img width="75px" height="75px" src={{ item.picture.url }}>
    {% endif %}
    </td>
    <td style="width:20%">
    {{ item.name }}<br>{{ item.cas }}<br>{{ item.price }}Yen
    </td>
    <td>
    {{ item.quantity }}Amounts<br>
    {% if not item.in_stock %}
      Exceed stock amounts so revise the amounts!
    {% endif %}
    </td>
  </tr>
{% endfor %}
</tbody>
</table>
<hr>
<h3 class="offset-9">Total: {{ total_price }}Yen</h3>
{% else %}
<h2>There is no item in a cart</h2>
{% endif %}
</div>
{% endblock %}

“ec_site/templates/base.html"に追記し,以下のように編集する.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  </head>
  <body>
    <nav class="navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'account:top' %}">&nbsp;Top</a>
      {% if user.is_authenticated %}
      <a class="navbar-brand" href="{% url 'account:logout' %}">Logout</a>
      <a class="navbar-brand" href="{% url 'chem_store:chemical_list' %}">Chemical List</a>
      <a class="navbar-brand" href="{% url 'chem_store:cart_items' %}">Go to cart</a> # 追記箇所
      {% else %}
      <a class="navbar-brand" href="{% url 'account:login' %}">Login</a>
      <a class="navbar-brand" href="{% url 'account:registration' %}">Registration</a>
      {% endif %}
      <a class="navbar-brand" href="{% url 'account:user' %}">User Page</a>
    </nav>
    {% block content %}{% endblock %}
  </body>
</html>

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 10, 2021 - 15:17:31
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/chem_store/cart_items"を入力すると以下カート精算画面に遷移する.化学物質名とCAS番号,個数,合計費用が表示される.

上記の精算ページの個数を変更できるように実装する.

“ec_site/chem_store"フォルダに以下のように"forms.py"を作成する.

“ec_site/chem_store/forms.py"を以下のように編集する.

from django import forms
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError

from .models import CartItems

class CartUpdateForm(forms.ModelForm):
    quantity = forms.IntegerField(label='Amounts', min_value=1)
    id = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = CartItems
        fields = ['quantity', 'id']

    def clean(self):
        cleaned_data = super().clean()
        quantity = cleaned_data.get('quantity')
        id = cleaned_data.get('id')
        cart_item = get_object_or_404(CartItems, pk=id)
        if quantity > cart_item.chemical.stock:
            raise ValidationError(f'Exceed stock amounts! Please fill item at or less than {cart_item.chemical.stock}!')

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import ( # 追記箇所(8~11行目)
    UpdateView, DeleteView,
)
from django.urls import reverse_lazy

import os
from .models import(
    Chemicals, Carts, CartItems
)
from .forms import( # 追記箇所(17~19行目)
    CartUpdateForm,
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView): # 以下追記箇所
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
    add_chemical, CartItemsView,
    CartUpdateView, CartDeleteView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'),
    path('cart_update/<int:pk>', CartUpdateView.as_view(), name='cart_update'), # 以下追記箇所
    path('cart_delete/<int:pk>', CartDeleteView.as_view(), name='cart_delete'),
]

“ec_site/chem_store/views.py"で"cart_update.html"と"cart_delete.html"を記入したので,"ec_site/templates/chem_store"フォルダに以下のように"cart_update.html"と"cart_delete.htmlを作成する.

“ec_site/templates/chem_store/cart_update.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="update">
</form>
{% endblock %}

“ec_site/templates/chem_store/cart_delete.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<h2>Do you want to delete {{ object.cheical.name }} from cart?</h2>
<form method="POST">
{% csrf_token %}
<input type="submit" value="delete">
</form>
{% endblock %}

“ec_site/templates/chem_store/cart_items.html"に追加し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
{% if items %}
<table>
<tbody>
{% for item in items %}
  <tr>
    <td style="width:20%">
    {% if item.picture %}
    <img width="75px" height="75px" src={{ item.picture.url }}>
    {% endif %}
    </td>
    <td style="width:20%">
    {{ item.name }}<br>{{ item.cas }}<br>{{ item.price }}Yen
    </td>
    <td>
    {{ item.quantity }}Amounts<br>
    {% if not item.in_stock %}
      Exceed stock amounts so revise the amounts!
    {% endif %}
    </td>
    <td align="right"> # 追記箇所(23~26行目)
      <a class="btn btn-primary" href="{% url 'chem_store:cart_update' pk=item.id %}">Edit</a>
      <a class="btn btn-danger" href="{% url 'chem_store:cart_delete' pk=item.id %}">Delete</a>
    </td>
  </tr>
{% endfor %}
</tbody>
</table>
<hr>
<h3 class="offset-9">Total: {{ total_price }}Yen</h3>
{% else %}
<h2>There is no item in a cart</h2>
{% endif %}
</div>
{% endblock %}

上記を保存し,"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/chem_store/cart_items/"を入力すると以下画面に遷移する."Edit"ボタンと"Delete"ボタンが実装された."Methanol"の"Edit"ボタンをクリックする.

以下画面に遷移する.

数量を実際のストックよりも大きくすると以下のようなメッセージが表示される.

数量を"2″から"5″に変更し,"update"ボタンをクリックする.

上記のクリック後,以下画面に遷移する.赤枠の数量が"2″から"5″に変更された.続けて,赤枠の"Delete"ボタンをクリックする.

以下画面に遷移するので,"delete"ボタンをクリックする

上記のクリック後,以下画面に遷移する.CAS No. 9006-24-0がカートから削除された.

画面の表示内容を変更するため,"ec_site/templates/base.html"に追記し,以下のように編集する.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  </head>
  <body>
    <nav class="navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'account:top' %}">&nbsp;Top</a>
      {% if user.is_authenticated %}
      <a class="navbar-brand" href="{% url 'account:logout' %}">Logout</a>
      <a class="navbar-brand" href="{% url 'chem_store:chemical_list' %}">Chemical List</a>
      <a class="navbar-brand" href="{% url 'chem_store:cart_items' %}">Go to cart</a> # 追記箇所
      {% else %}
      <a class="navbar-brand" href="{% url 'account:login' %}">Login</a>
      <a class="navbar-brand" href="{% url 'account:registration' %}">Registration</a>
      {% endif %} # 削除箇所(19行目を削除した.以下は削除後になる)
    </nav>
    {% block content %}{% endblock %}
  </body>
</html>

上記を保存後,以下画面に移動する.不要のため,上部の"User Page"を削除した.

“ec_site/templates/chem_store/cart_update.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<p>{{ object.chemical.name }}(CAS No. {{ object.chemical.cas }}): {{ object.chemical.stock }} Amounts</p>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="update">
</form>
{% endblock %}

上記を保存後,以下画面に移動する."Edit"ボタンをクリックする.

上記ボタンをクリック後,以下画面に遷移する.以下赤枠のように化学物質名,CAS番号,ストック数量が表示された.

  1. 住所情報の追加実装

“ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager):

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager()

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]
        
class Addresses(models.Model): # 以下追記箇所
    zip_code = models.CharField(max_length=8)
    prefecture = models.CharField(max_length=10)
    address = models.CharField(max_length=250)
    user = models.ForeignKey(
        MyUser,
        on_delete = models.CASCADE,
    )

    class Meta:
        db_table = 'addresses'

    def __str__(self):
        return f'{self.zip_code} {self.prefecture} {self.address}'

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py makemigrations chem_store”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py makemigrations chem_store

Migrations for 'chem_store': # 以下出力箇所
  chem_store\migrations\0003_addresses.py
    - Create model Addresses

続けて,ターミナルにて"python manage.py migrate"を入力する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py migrate
Operations to perform: # 以下出力箇所
  Apply all migrations: account, admin, auth, chem_store, contenttypes, sessions
Running migrations:
  Applying chem_store.0003_addresses... OK

SQLiteをインストールし,"SQLITE EXPLORER"を開くと,"addresses"テーブルを以下赤枠に確認できる.

“ec_site/templates/chem_store/cart_items.html"に追記し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
{% if items %}
<table>
<tbody>
{% for item in items %}
  <tr>
    <td style="width:20%">
    {% if item.picture %}
    <img width="75px" height="75px" src={{ item.picture.url }}>
    {% endif %}
    </td>
    <td style="width:20%">
    {{ item.name }}<br>{{ item.cas }}<br>{{ item.price }}Yen
    </td>
    <td>
    {{ item.quantity }}Amounts<br>
    {% if not item.in_stock %}
      Exceed stock amounts so revise the amounts!
    {% endif %}
    </td>
    <td align="right">
      <a class="btn btn-primary" href="{% url 'chem_store:cart_update' pk=item.id %}">Edit</a>
      <a class="btn btn-danger" href="{% url 'chem_store:cart_delete' pk=item.id %}">Delete</a>
    </td>
  </tr>
{% endfor %}
</tbody>
</table>
<hr>
<h3 class="offset-9">Total: {{ total_price }}Yen</h3>
<a class="offset-9 btn btn-secondary" href="{% url 'chem_store:enter_address' %}">Enter address</a> # 追記箇所
{% else %}
<h2>There is no item in a cart</h2>
{% endif %}
</div>
{% endblock %}

“ec_site/chem_store/forms.py"に追記し,以下のように編集する.

from django import forms
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError

from .models import Addresses, CartItems

class CartUpdateForm(forms.ModelForm):
    quantity = forms.IntegerField(label='Amounts', min_value=1)
    id = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = CartItems
        fields = ['quantity', 'id']

    def clean(self):
        cleaned_data = super().clean()
        quantity = cleaned_data.get('quantity')
        id = cleaned_data.get('id')
        cart_item = get_object_or_404(CartItems, pk=id)
        if quantity > cart_item.chemical.stock:
            raise ValidationError(f'Exceed stock amounts! Please fill item at or less than {cart_item.chemical.stock}!')

class EnterAddressForm(forms.ModelForm): # 以下追記箇所
    address = forms.CharField(label='Address', widget=forms.TextInput(attrs={'size':'70'}))

    class Meta:
        model = Addresses
        fields = ['zip_code', 'prefecture', 'address']
        labels = {
            'zip_code': 'Zip code',
            'prefecture': 'Prefecture',
        }

    def save(self):
        address = super().save(commit=False)
        address.user = self.user
        address.save()
        return address

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import ( # 変更箇所
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy

import os
from .models import(
    Chemicals, Carts, CartItems
)
from .forms import( # 変更箇所
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView): # 以下追記箇所
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request)

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
    add_chemical, CartItemsView,
    CartUpdateView, CartDeleteView,
    EnterAddressView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'),
    path('cart_update/<int:pk>', CartUpdateView.as_view(), name='cart_update'),
    path('cart_delete/<int:pk>', CartDeleteView.as_view(), name='cart_delete'),
    path('enter_address/', EnterAddressView.as_view(), name='enter_address'), # 以下追記箇所
]

“ec_site/chem_store/views.py"で"enter_address.html"を記載しているので,"ec_site/templates/chem_store"に"enter_address.html"を以下のように作成する.

“ec_site/templates/chem_store/enter_address.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
  <h2>set address</h2>
  <hr>
  <form method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="set address">
  </form>
</div>
{% endblock %}

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 12, 2021 - 00:54:13
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Go to cart"をクリックする.

以下カート画面に遷移する.赤枠の"Enter address"をクリックする.

以下住所入力ページに遷移する.

以下のように住所を入力し,"set address"をクリックする.

以下画面に遷移する.

SQLiteをインストールし,"SQLITE EXPLORER"を開き,"addresses"テーブルを右クリックし,"Show Table"をクリックする.以下情報が格納されたことを確認できる.

住所の情報をキャッシュに保存するため,"ec_site/chem_store/forms.py"に追記し,以下のように編集する.

from django import forms
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
from django.core.cache import cache # 追記箇所
from .models import Addresses, CartItems

class CartUpdateForm(forms.ModelForm):
    quantity = forms.IntegerField(label='Amounts', min_value=1)
    id = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = CartItems
        fields = ['quantity', 'id']

    def clean(self):
        cleaned_data = super().clean()
        quantity = cleaned_data.get('quantity')
        id = cleaned_data.get('id')
        cart_item = get_object_or_404(CartItems, pk=id)
        if quantity > cart_item.chemical.stock:
            raise ValidationError(f'Exceed stock amounts! Please fill item at or less than {cart_item.chemical.stock}!')

class EnterAddressForm(forms.ModelForm):
    address = forms.CharField(label='Address', widget=forms.TextInput(attrs={'size':'70'}))

    class Meta:
        model = Addresses
        fields = ['zip_code', 'prefecture', 'address']
        labels = {
            'zip_code': 'Zip code',
            'prefecture': 'Prefecture',
        }

    def save(self):
        address = super().save(commit=False)
        address.user = self.user
        address.save()
        cache.set(f'address_user_{self.user.id}', address) # 追記箇所
        return address

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache # 追記箇所

import os
from .models import(
    Chemicals, Carts, CartItems
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request)

    def get_context_data(self, **kwargs): # 追記箇所(148~155行目)
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

上記を保存し,ブラウザのURLから"http://127.0.0.1:8000/account/top"をに移動し,"Go to cart"をクリックする.以下画面に遷移するので,"Enter address"をクリックする.

以下住所入力画面に遷移する.

以下のように住所を入力し,"set address"をクリックする.

以下画面に遷移するので,再度"Enter address"をクリックする.

以下住所入力画面に遷移すると上記で入力した値が保存されているのが分かる.

過去に入力した住所一覧を選択できるように実装する."ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import( # 変更箇所
    Chemicals, Carts, CartItems, Addresses
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request, pk=None): # 変更箇所
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk) # 変更箇所

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk') # 追記箇所(151~152行目)
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all() # 追記箇所
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

“ec_site/templates/chem_store/enter_address.html"に追加し,以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<div class="col-10 offset-1">
  <h2>set address</h2>
  <hr>
  <form method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit" value="set address">
  </form>
  <hr> # 追記箇所(11~15行目)
  <h3>Address list which was used in past</h3>
  {% for address in addresses %}
    <p><a href={% url 'chem_store:enter_address' pk=address.id %}>{{ address }}</a></p>
  {% endfor %}
</div>
{% endblock %}

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import (
    ChemicalListView, ChemicalDetailView,
    add_chemical, CartItemsView,
    CartUpdateView, CartDeleteView,
    EnterAddressView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'),
    path('cart_update/<int:pk>', CartUpdateView.as_view(), name='cart_update'),
    path('cart_delete/<int:pk>', CartDeleteView.as_view(), name='cart_delete'),
    path('enter_address/', EnterAddressView.as_view(), name='enter_address'),
    path('enter_address/<int:pk>', EnterAddressView.as_view(), name='enter_address'), # 以下追記箇所
]

上記を保存し,ブラウザのURLから"http://127.0.0.1:8000/account/top"をに移動し,"Go to cart"をクリックする.以下画面に遷移するので,"Enter address"をクリックする.

以下画面に遷移する.過去設定した住所一覧を確認することができる.

その中の1つの住所のリンクをクリックすると,上記の赤枠に情報が移動することが分かる.

過去の履歴から入力すると以下のように重複するデータが出てくる.

上記の過去の履歴に出力するデータでは,重複は避けるような実装を行う."ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager):

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager()

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]
        
class Addresses(models.Model):
    zip_code = models.CharField(max_length=8)
    prefecture = models.CharField(max_length=10)
    address = models.CharField(max_length=250)
    user = models.ForeignKey(
        MyUser,
        on_delete = models.CASCADE,
    )

    class Meta:
        db_table = 'addresses'
        unique_together = [ # 追記箇所
            ['zip_code', 'prefecture', 'address', 'user']
        ]

    def __str__(self):
        return f'{self.zip_code} {self.prefecture} {self.address}'

“class Addresses"を変更したので,ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py makemigrations chem_store”を実行する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py makemigrations chem_store
Migrations for 'chem_store': # 以下出力箇所
  chem_store\migrations\0004_alter_addresses_unique_together.py
    - Alter unique_together for addresses (1 constraint(s))

このままマイグレートするのでエラーが出現する.そのため,マイグレートする前に,"SQLITE EXPLORER"を開き,"Addresses"テーブルを右クリックして,赤枠の"New Query[Select]"をクリックする.

以下が記載されている.

-- SQLite
SELECT id, zip_code, prefecture, address, user_id
FROM addresses;

以下のように変更する.

-- SQLite
DELETE FROM addresses;

右クリックをすると以下が出現するので,"Run Query"をクリックすると,"Addresses"テーブルからすべてのデータが削除された.

住所情報を削除したので,ターミナルで"python manage.py migrate chem_store"を実行すると以下が出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site>python manage.py migrate chem_store

Operations to perform: # 以下出力箇所
  Apply all migrations: chem_store
Running migrations:
  Applying chem_store.0004_alter_addresses_unique_together... OK

“ec_site/chem_store/forms.py"に追記し,以下のように編集する.

from django import forms
from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
from django.core.cache import cache
from .models import Addresses, CartItems

class CartUpdateForm(forms.ModelForm):
    quantity = forms.IntegerField(label='Amounts', min_value=1)
    id = forms.CharField(widget=forms.HiddenInput())

    class Meta:
        model = CartItems
        fields = ['quantity', 'id']

    def clean(self):
        cleaned_data = super().clean()
        quantity = cleaned_data.get('quantity')
        id = cleaned_data.get('id')
        cart_item = get_object_or_404(CartItems, pk=id)
        if quantity > cart_item.chemical.stock:
            raise ValidationError(f'Exceed stock amounts! Please fill item at or less than {cart_item.chemical.stock}!')

class EnterAddressForm(forms.ModelForm):
    address = forms.CharField(label='Address', widget=forms.TextInput(attrs={'size':'70'}))

    class Meta:
        model = Addresses
        fields = ['zip_code', 'prefecture', 'address']
        labels = {
            'zip_code': 'Zip code',
            'prefecture': 'Prefecture',
        }

    def save(self):
        address = super().save(commit=False)
        address.user = self.user
        try: # 追記箇所(37~47行目)
            address.validate_unique()
            address.save()
        except ValidationError as e:
            address = get_object_or_404(
                Addresses, user=self.user,
                prefecture=address.prefecture,
                zip_code=address.zip_code,
                address=address.address,
            )
            pass
        cache.set(f'address_user_{self.user.id}', address)
        return address

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 12, 2021 - 16:55:05
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力し,"Go to cart"をクリックすると以下画面に遷移する."Enter address"をクリックする.

以下住所入力画面に遷移する.上記でデータを削除したので履歴がない状態である.

以下のように情報を記入し,"set address"をクリックする.

以下画面に遷移するので,"Enter address"をクリックする.

以下画面に遷移する.過去の履歴が保存されている.過去の履歴情報と同じ情報を入力し,"set address"をクリックする.

再度以下画面に移動すると重複なく履歴が記載されていることが分かる.

カート情報を注文テーブルへ移動実装する."ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager):

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager()

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]
        
class Addresses(models.Model):
    zip_code = models.CharField(max_length=8)
    prefecture = models.CharField(max_length=10)
    address = models.CharField(max_length=250)
    user = models.ForeignKey(
        MyUser,
        on_delete = models.CASCADE,
    )

    class Meta:
        db_table = 'addresses'
        unique_together = [
            ['zip_code', 'prefecture', 'address', 'user']
        ]

    def __str__(self):
        return f'{self.zip_code} {self.prefecture} {self.address}'

class Orders(models.Model): # 以下追記箇所
    total_price = models.PositiveIntegerField()
    address = models.ForeignKey(
        Addresses,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    user = models.ForeignKey(
        MyUser,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

    class Meta:
        db_table = 'orders'

class OrdersItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    order = models.ForeignKey(
        Orders, on_delete=models.CASCADE
    )

    class Meta:
        db_table = 'order_items'
        unique_together = [['chemical', 'order']]

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py makemigrations chem_store”を実行する.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py makemigrations chem_store
Migrations for 'chem_store': # 以下出力箇所
  chem_store\migrations\0005_orders_ordersitems.py
    - Create model Orders
    - Create model OrdersItems

続けて,ターミナルにて"python manage.py migrate chem_store"を実行すると以下が出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py migrate chem_store
Operations to perform: # 以下出力箇所
  Apply all migrations: chem_store
Running migrations:
  Applying chem_store.0005_orders_ordersitems... OK

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from typing import ItemsView
from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView): # 以下追記箇所
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

“ec_site/chem_store/urls.py"を追記し,以下のように編集する.

from django.urls import path
from .views import ( # 変更箇所
    ChemicalListView, ChemicalDetailView,
    add_chemical, CartItemsView,
    CartUpdateView, CartDeleteView,
    EnterAddressView, ConfirmOrderView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'),
    path('cart_update/<int:pk>', CartUpdateView.as_view(), name='cart_update'),
    path('cart_delete/<int:pk>', CartDeleteView.as_view(), name='cart_delete'),
    path('enter_address/', EnterAddressView.as_view(), name='enter_address'),
    path('enter_address/<int:pk>', EnterAddressView.as_view(), name='enter_address'),
    path('confirm_order/', ConfirmOrderView.as_view(), name='confirm_order'), # 以下追記箇所
]

“ec_site/chem_store/views.py"で"confirm_order.html"を記載したので,"ec_site/templates/chem_store"フォルダに"confirm_order.html"を以下のように作成する.

“ec_site/templates/chem_store/confirm_order.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<p>{{ user.username }} san, please confirm the order!</p>
<p>Zip code: {{ address.zip_code }}</p>
<p>Prefecture: {{ address.prefecture }}</p>
<p>Address: {{ address.address }}</p>
<table>
<tbody>
  {% for item in items %}
    <tr>
      <td style="width: 20%;">
        {% if item.picture %}
          <img width="75px" height="75px" src={{ item.picture.url }}>
        {% endif %}
      </td>
      <td style="width: 20%">{{ item.name }}<br>{{ item.price }} Yen</td>
      <td align="right">{{ item.quantity }} Amounts</td>
    </tr>
  {% endfor %}
</tbody>
</table>
<hr>
<p><a href="{% url 'chem_store:cart_items' %}">Go to cart</a></p>
<p><a href="{% url 'chem_store:enter_address' %}">Enter address</a></p>
<h3 class="offset-9">Total: {{ total_price }} Yen</h3>
<br>
<form method="POST" class="offset-10">
{% csrf_token %}
<input type="submit" class="btn btn-primary" value="decide order">
</form>
{% endblock %}

“ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager):

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager()

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]
        
class Addresses(models.Model):
    zip_code = models.CharField(max_length=8)
    prefecture = models.CharField(max_length=10)
    address = models.CharField(max_length=250)
    user = models.ForeignKey(
        MyUser,
        on_delete = models.CASCADE,
    )

    class Meta:
        db_table = 'addresses'
        unique_together = [
            ['zip_code', 'prefecture', 'address', 'user']
        ]

    def __str__(self):
        return f'{self.zip_code} {self.prefecture} {self.address}'

class OrdersManager(models.Manager): # 追記箇所(101~108行目)

    def insert_cart(self, cart:Carts, address, total_price):
       return self.create(
           total_price=total_price,
           address=address,
           user=cart.user,
       ) 

class Orders(models.Model):
    total_price = models.PositiveIntegerField()
    address = models.ForeignKey(
        Addresses,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    user = models.ForeignKey(
        MyUser,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

    class Meta:
        db_table = 'orders'

class OrdersItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    order = models.ForeignKey(
        Orders, on_delete=models.CASCADE
    )

    class Meta:
        db_table = 'order_items'
        unique_together = [['chemical', 'order']]

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from typing import ItemsView
from django.shortcuts import render, get_object_or_404
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import( # 変更箇所
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    def post(self, request, *args, **kwargs): # 以下追記箇所
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.product.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)

“ec_site/chem_store/models.py"に追記し,以下のように編集する.

from django.db import models
from account.models import MyUser

class ChemicalTypes(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'chemical_types'

    def __str__(self):
        return self.name

class Manufacturers(models.Model):
    name = models.CharField(max_length=300)

    class Meta:
        db_table = 'manufacturers'

    def __str__(self):
        return self.name

class ChemicalsManager(models.Manager): # 追記箇所(22~28行目)

    def reduce_stock(self, cart):
        for item in cart.cartitems_set.all():
            update_stock = item.chemical.stock - item.quantity
            item.chemical.stock = update_stock
            item.chemical.save()

class Chemicals(models.Model):
    name = models.CharField(max_length=300)
    cas = models.CharField(max_length=300)
    price = models.IntegerField()
    stock = models.IntegerField()
    chemical_type = models.ForeignKey(
        ChemicalTypes, on_delete=models.CASCADE
    )
    manufacturer = models.ForeignKey(
        Manufacturers, on_delete=models.CASCADE
    )
    objects = ChemicalsManager() # 追記箇所

    class Meta:
        db_table = 'chemicals'

    def __str__(self):
        return self.name

class ChemicalPictures(models.Model):
    picture = models.FileField(upload_to='chemical_pictures/')
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    order = models.IntegerField()

    class Meta:
        db_table = 'chemical_pictures'
        ordering = ['order']

    def __str__(self):
        return self.chemical.name + ': ' + str(self.order)
        
class Carts(models.Model):
    user = models.OneToOneField(
        MyUser,
        on_delete=models.CASCADE,
        primary_key=True
    )

    class Meta:
        db_table = 'carts'

class CartItemsManager(models.Manager):

    def save_item(self, chemical_id, quantity, cart):
        c = self.model(quantity=quantity, chemical_id=chemical_id, cart=cart)
        c.save()

class CartItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals, on_delete=models.CASCADE
    )
    cart = models.ForeignKey(
        Carts, on_delete=models.CASCADE
    )
    objects = CartItemsManager()

    class Meta:
        db_table = 'cart_items'
        unique_together = [['chemical', 'cart']]
        
class Addresses(models.Model):
    zip_code = models.CharField(max_length=8)
    prefecture = models.CharField(max_length=10)
    address = models.CharField(max_length=250)
    user = models.ForeignKey(
        MyUser,
        on_delete = models.CASCADE,
    )

    class Meta:
        db_table = 'addresses'
        unique_together = [
            ['zip_code', 'prefecture', 'address', 'user']
        ]

    def __str__(self):
        return f'{self.zip_code} {self.prefecture} {self.address}'

class OrdersManager(models.Manager):

    def insert_cart(self, cart:Carts, address, total_price):
       return self.create(
           total_price=total_price,
           address=address,
           user=cart.user,
       ) 

class Orders(models.Model):
    total_price = models.PositiveIntegerField()
    address = models.ForeignKey(
        Addresses,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    user = models.ForeignKey(
        MyUser,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    objects = OrdersManager() # 追記箇所

    class Meta:
        db_table = 'orders'

class OrdersItemsManager(models.Manager): # 追記箇所(129~137行目)

    def insert_cart_items(self, cart, order):
        for item in cart.cartitems_set.all():
            self.create(
                quantity=item.quantity,
                chemical=item.chemical,
                order=order,
            )

class OrdersItems(models.Model):
    quantity = models.PositiveIntegerField()
    chemical = models.ForeignKey(
        Chemicals,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    order = models.ForeignKey(
        Orders, on_delete=models.CASCADE
    )
    objects = OrdersItemsManager() # 追記箇所

    class Meta:
        db_table = 'order_items'
        unique_together = [['chemical', 'order']]

“ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import ( # 変更箇所
    render, redirect, get_object_or_404
)
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:cart_items')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    def post(self, request, *args, **kwargs):
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.product.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)
        Chemicals.objects.reduce_stock(cart) # 以下追記箇所
        cart.delete()
        return redirect(reverse_lazy('chem_store:order_success'))


class OrderSuccessView(LoginRequiredMixin, TemplateView):

    template_name = os.path.join('chem_store', 'order_success.html')

“ec_site/chem_store/urls.py"に追記し,以下のように編集する.

from django.urls import path
from .views import (
    ChemicalListView, ChemicalDetailView, OrderSuccessView,
    add_chemical, CartItemsView,
    CartUpdateView, CartDeleteView,
    EnterAddressView, ConfirmOrderView,
)

app_name='chem_store'
urlpatterns = [
    path('chemical_list/', ChemicalListView.as_view(), name='chemical_list'),
    path('chemical_detail/<int:pk>', ChemicalDetailView.as_view(), name='chemical_detail'),
    path('add_chemical/', add_chemical, name='add_chemical'),
    path('cart_items/', CartItemsView.as_view(), name='cart_items'),
    path('cart_update/<int:pk>', CartUpdateView.as_view(), name='cart_update'),
    path('cart_delete/<int:pk>', CartDeleteView.as_view(), name='cart_delete'),
    path('enter_address/', EnterAddressView.as_view(), name='enter_address'),
    path('enter_address/<int:pk>', EnterAddressView.as_view(), name='enter_address'),
    path('confirm_order/', ConfirmOrderView.as_view(), name='confirm_order'),
    path('order_success/', OrderSuccessView.as_view(), name='order_success'), # 以下追記箇所
]

“ec_site/chem_store/views.py"に"order_success.html"を記載したので,"ec_site/templates/chem_store"に"order_success.html"を以下のように作成する.

“ec_site/templates/chem_store/order_success.html"を以下のように編集する.

{% extends 'base.html' %}
{% block content %}
<h2>Received the order!</h2>
{% endblock %}

“ec_site/chem_store/views.py"を以下のように編集する.

from django.shortcuts import (
    render, redirect, get_object_or_404
)
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:confirm_order') # 変更箇所

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    def post(self, request, *args, **kwargs):
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.chemical.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)
        Chemicals.objects.reduce_stock(cart)
        cart.delete()
        return redirect(reverse_lazy('chem_store:order_success'))


class OrderSuccessView(LoginRequiredMixin, TemplateView):

    template_name = os.path.join('chem_store', 'order_success.html')

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 14, 2021 - 01:41:45
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Go to cart"をクリックする.

以下画面に遷移するので,"Enter address"をクリックする.

以下住所設定画面に遷移する.

以下のように記述し,"set address"をクリックする.

以下画面に遷移する."Go to cart", “Enter address", “decide order"を各々クリックする.まずは"Go to cart"をクリックする.

上記の"Go to cart"をクリックすると,以下画面に遷移する.

上記の"Enter address"をクリックすると,以下画面に遷移する.

上記の"decide order"をクリックすると,以下画面に遷移する.注文が受け付けられた.続けて,赤枠の"Chemical List"をクリックする.

以下化学物質リスト画面に遷移する.注文をしたので赤枠の在庫数が減少した."Go to cart"をクリックする.

以下画面に遷移する.カートにはアイテムがないことが分かる.

“SQLITE EXPLORER"を開き,赤枠の"order items"と"order"を右クリックして"Show Table"をクリックする.

“order items"は以下のようなデータが格納されている.
※データ格納が重複されていたので,データを削除し,再度購入したので,idが5から始まっている.

“order"は以下のようなデータが格納されている.

  1. 原子性を含むデータベースの実装

ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 14, 2021 - 13:54:45
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Chemical List"をクリックする.

各化学物質の在庫は以下赤枠になっている.

“ec_site/chem_store/views.py"の一部を意図的に以下のように変更する.(“reduce_stock" => “red_stock")

from django.shortcuts import (
    render, redirect, get_object_or_404
)
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:confirm_order')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    def post(self, request, *args, **kwargs):
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.chemical.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)
        Chemicals.objects.red_stock(cart) # 変更箇所
        cart.delete()
        return redirect(reverse_lazy('chem_store:order_success'))


class OrderSuccessView(LoginRequiredMixin, TemplateView):

    template_name = os.path.join('chem_store', 'order_success.html')

変更後,化学物質"Methanol"を1個カートに入れる.在庫は5個ある.

以下画面に遷移する.

以下画面で発注をする.

上記の"ec_site/chem_store/views.py"で変更したので,以下エラーが発生する.

“SQLITE EXPLORER"を開き,"cart_items"と"carts"テーブルを右クリックして"Show Table"をクリックすると以下データが確認できる.

“Chemical List"のページを確認すると以下のように数量が減っていない.

上記のような状況が起こる理由は,データベースの原子性が含まれていないためである.そのため,原子性が含むような実装を行う."ec_site/chem_store/views.py"に追記し,以下のように編集する.

from django.shortcuts import (
    render, redirect, get_object_or_404
)
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db import transaction # 追記箇所

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:confirm_order')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    @transaction.atomic # 追記箇所
    def post(self, request, *args, **kwargs):
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.chemical.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)
        Chemicals.objects.red_stock(cart) #
        cart.delete()
        return redirect(reverse_lazy('chem_store:order_success'))


class OrderSuccessView(LoginRequiredMixin, TemplateView):

    template_name = os.path.join('chem_store', 'order_success.html')

上記を保存し,項目5の上記で実施したアクションを再度実行する.ターミナルを開き,仮想環境に移行し,"cd ec_site"を入力し,ディレクトリを変更する.変更後,以下のように”python manage.py runserver”を実行する.実行後,以下のように出力される.

(djangoenv) C:\Users\shiro\Desktop\210517_python development\myclassbasedviewslogintest\ec_site
>python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced). # 以下出力箇所
September 14, 2021 - 15:27:15
Django version 3.2.3, using settings 'ec_site.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

上記の"http://127.0.0.1:8000/"をクリックするとブラウザが開くので,URLに"http://127.0.0.1:8000/account/top"を入力すると以下画面に遷移する."Chemical List"をクリックする.

現状以下のように化学物質"Methanol"の在庫数は5個となっている.

カードにはすでに1つの化学物質が入っている状態となっている.

“Go to cart"のページである以下に遷移する.

以下のように注文ページに移動する."decide order"をクリックする.

以下のようにエラーが発生する.

“SQLITE EXPLORER"を開き,"cart_items"と"carts"テーブルを右クリックして"Show Table"をクリックすると以下データが確認できる.前回と同じ内容になっているので,追加のオーダーは各テーブルに入っていないことが分かる.

上記が確認できたので,"ec_site/chem_store/views.py"を以下のように編集する.

from django.shortcuts import (
    render, redirect, get_object_or_404
)
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
    UpdateView, DeleteView, CreateView
)
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db import transaction

import os
from .models import(
    Chemicals, Carts, CartItems, Addresses,
    Orders, OrdersItems,
)
from .forms import(
    CartUpdateForm, EnterAddressForm
)

class ChemicalListView(LoginRequiredMixin, ListView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_list.html')

    def get_queryset(self):
        query = super().get_queryset()
        chemical_type_name = self.request.GET.get('chemical_type_name', None)
        chemical_name = self.request.GET.get('chemical_name', None)
        cas_name = self.request.GET.get('cas_name', None)
        if chemical_type_name:
            query = query.filter(
                chemical_type__name=chemical_type_name
            )
        if chemical_name:
            query = query.filter(
                name=chemical_name
            )
        if cas_name:
            query = query.filter(
                cas=cas_name
            )
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            query = query.order_by('price')
        elif order_by_price == '2':
            query = query.order_by('-price')
        return query

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['chemical_type_name'] = self.request.GET.get('chemical_type_name', '')
        context['chemical_name'] = self.request.GET.get('chemical_name', '')
        context['cas_name'] = self.request.GET.get('cas_name', '')
        order_by_price = self.request.GET.get('order_by_price', 0)
        if order_by_price == '1':
            context['ascending'] = True
        elif order_by_price == '2':
            context['descending'] = True
        return context

class ChemicalDetailView(LoginRequiredMixin, DetailView):
    model = Chemicals
    template_name = os.path.join('chem_store', 'chemical_detail.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['is_added'] = CartItems.objects.filter(
            cart_id=self.request.user.id,
            chemical_id=kwargs.get('object').id
        ).first()
        return context

@login_required
def add_chemical(request):
    if request.is_ajax:
        chemical_id = request.POST.get('chemical_id')
        quantity = request.POST.get('quantity')
        chemical = get_object_or_404(Chemicals, id=chemical_id)
        if int(quantity) > chemical.stock:
            response = JsonResponse({'message': 'exceed stock amounts!'})
            response.status_code = 403
            return response
        if int(quantity) <= 0:
            response = JsonResponse({'message': 'enter amount more than zero!'})
            response.status_code = 403
            return response
        cart = Carts.objects.get_or_create(
            user=request.user
        )
        if all([chemical_id, cart, quantity]):
            CartItems.objects.save_item(
                quantity=quantity, chemical_id=chemical_id,
                cart=cart[0]
            )
            return JsonResponse({'message': 'added item to cart!'})

class CartItemsView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'cart_items.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_id = self.request.user.id
        query = CartItems.objects.filter(cart_id=user_id)
        total_price = 0
        items = []
        for item in query.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            in_stock = True if item.chemical.stock >= item.quantity else False
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'cas': item.chemical.cas,
                'id': item.id,
                'price': item.chemical.price,
                'in_stock': in_stock,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

class CartUpdateView(LoginRequiredMixin, UpdateView):
    template_name = os.path.join('chem_store', 'cart_update.html')
    form_class = CartUpdateForm
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class CartDeleteView(LoginRequiredMixin, DeleteView):
    template_name = os.path.join('chem_store', 'cart_delete.html')
    model = CartItems
    success_url = reverse_lazy('chem_store:cart_items')

class EnterAddressView(LoginRequiredMixin, CreateView):
    template_name = os.path.join('chem_store', 'enter_address.html')
    form_class = EnterAddressForm
    success_url = reverse_lazy('chem_store:confirm_order')

    def get(self, request, pk=None):
        cart = get_object_or_404(Carts, user_id=request.user.id)
        if not cart.cartitems_set.all():
            raise Http404('No item in cart')
        return super().get(request, pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        pk = self.kwargs.get('pk')
        address = get_object_or_404(Addresses, user_id=self.request.user.id, pk=pk) if pk else address
        if address:
            context['form'].fields['zip_code'].initial = address.zip_code
            context['form'].fields['prefecture'].initial = address.prefecture
            context['form'].fields['address'].initial = address.address
        context['addresses'] = Addresses.objects.filter(user=self.request.user).all()
        return context

    def form_valid(self, form):
        form.user = self.request.user
        return super().form_valid(form)

class ConfirmOrderView(LoginRequiredMixin, TemplateView):
    template_name = os.path.join('chem_store', 'confirm_order.html')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        address = cache.get(f'address_user_{self.request.user.id}')
        context['address'] = address
        cart = get_object_or_404(Carts, user_id=self.request.user.id)
        context['cart'] = cart
        total_price = 0
        items = []
        for item in cart.cartitems_set.all():
            total_price += item.quantity * item.chemical.price
            picture = item.chemical.chemicalpictures_set.first()
            picture = picture.picture if picture else None
            temp_item = {
                'quantity': item.quantity,
                'picture': picture,
                'name': item.chemical.name,
                'price': item.chemical.price,
                'id': item.id,
            }
            items.append(temp_item)
        context['total_price'] = total_price
        context['items'] = items
        return context

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        context = self.get_context_data()
        address = context.get('address')
        cart = context.get('cart')
        total_price = context.get('total_price')
        if (not address) or (not cart) or (not total_price):
            raise Http404('Error occurs for order process!')
        for item in cart.cartitems_set.all():
            if item.quantity > item.chemical.stock:
                raise Http404('Error occurs for order process!')
        order = Orders.objects.insert_cart(cart, address,total_price)
        OrdersItems.objects.insert_cart_items(cart, order)
        Chemicals.objects.reduce_stock(cart) # 変更箇所
        cart.delete()
        return redirect(reverse_lazy('chem_store:order_success'))


class OrderSuccessView(LoginRequiredMixin, TemplateView):

    template_name = os.path.join('chem_store', 'order_success.html')
  1. 反省点

class-based viewsにおけるログインの実装方法」にてアプリ名を"account"と名付けたが,「OAuth認証を用いたgoogleでのログイン方法」で当該アプリ名はエラーを引き起こした.また,その他のエラーを発生したので,フォルダ「OAuth認証を用いたgoogleでのログイン方法」の項目1で作成したコードに基づき,当記事のコードを追加すれば,エラーなく,OAuth認証を用いたgoogleでのログインをすることができる.

  1. 参照

■cacheについて
https://docs.djangoproject.com/en/3.2/topics/cache/

■原子性について
https://docs.djangoproject.com/en/3.2/topics/db/transactions/#controlling-transactions-explicitly

以上