Published Date : 2020年6月27日22:21

【Django】Part 9 - Djangoを使って簡単なオンラインショップを作ってみよう
【Django】Part 9 - Create a Simple Online Shop with Django


This blog has an English translation


YouTubeにアップした動画、「【Django】Part 9 - Create a Simple Online Shop with Django」の補足説明の記事です。

Here's a little more about the 「【Django】Part 9 - Create a Simple Online Shop with Django」 video I uploaded to YouTube.

Djangoを使って簡単なログイン機能を持ったオンラインショップのようなサイトを作ってみましょう。 Part9となる今回の動画シリーズでは、商品をカートに入れて商品を購入できるようにします。 加えて、サイトの細かい表示を調整していきます。

Use Django to create a site that looks like an online store with a simple login feature. This video series, Part 9, will allow you to purchase items by putting them in a cart. In addition, we will adjust the detailed view of the site.

無駄な説明を省いて、忙しい時でも短時間で理解できるような動画です。

It's a video that can be understood in a short period of time even when you're busy, without any unnecessary explanation.


目次

Table of Contents




① modelsとurls
① models and urls



models.pyとurls.pyを実装していきませう。

Let's implement models.py and urls.py.


models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import FileExtensionValidator
import uuid
import os
# Create your models here.


class Customer(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.user.username

    class Meta:
        verbose_name = "カスタマー"
        verbose_name_plural = "カスタマー"


class Seller(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.user.username

    class Meta:
        verbose_name = "出品者"
        verbose_name_plural = "出品者"


def product_image_filename(instance, filename):
    ext = filename.split('.')[-1]
    uuidname = uuid.uuid4()
    filename = f"{uuidname}.{ext}"
    return os.path.join('product_images', str(instance.seller.id), filename)


class Item(models.Model):
    title = models.CharField(
        max_length=30,
        null=False,
        blank=False,
        unique=False)
    description = models.TextField(
        max_length=90,
        null=False,
        blank=False,
        unique=False)
    price = models.FloatField(
        null=False,
        blank=False,
        unique=False)
    seller = models.ForeignKey(
        Seller,
        on_delete=models.CASCADE)

    image = models.ImageField(verbose_name='商品画像',
                                validators=[
                                    FileExtensionValidator(['jpg', 'png'])],
                                upload_to=product_image_filename,
                                null=True,
                                default='product_images/000/000.png')

    stock = models.IntegerField(default=1)
    available = models.BooleanField(default=True)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "商品"
        verbose_name_plural = "商品"


class Transaction(models.Model):
    item = models.ForeignKey(
        Item,
        on_delete=models.CASCADE
    )
    customer = models.ForeignKey(
        Customer,
        on_delete=models.CASCADE
    )
    timestamp = models.DateField(default=timezone.now)
    select_num = models.IntegerField(default=1)
    added_to_a_cart = models.BooleanField(default=False)

前回書いたmodels.pyからの変更点は以下になります。

The change from models.py that we wrote last time is the following script.

.............................................................................
.............................................................................

class Item(models.Model):
    .............................................................................
    .............................................................................
    .............................................................................
    .............................................................................
    stock = models.IntegerField(default=1)
    available = models.BooleanField(default=True)
    .............................................................................
    .............................................................................

class Transaction(models.Model):
    .............................................................................
    .............................................................................
    .............................................................................
    .............................................................................
    select_num = models.IntegerField(default=1)
    added_to_a_cart = models.BooleanField(default=False)

stockは商品の在庫数、availableは商品の取引が可能か否かを表しています。stockがゼロになるとavailableはFalseとなり、取引不可能になります。

Stock indicates the number of products in stock, and available indicates whether or not the products can be traded. If stock is zero, available is False and the transaction is not possible.

select_numはカスタマーが選択した商品の数。added_to_a_cartはカートに商品が入っているかを判定します。

select_num is the number of products selected by the customer. added_to_a_cart determines if an item is in the cart.

次はurls.pyです。

Next is urls.py.


urls.py

from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views

app_name = 'demoshop'

urlpatterns = [
    path('', views.index, name='index'),
    path('home', views.home, name='home'),
    path('signin', views.signin, name='signin'),
    path('signup', views.signup, name='signup'),
    path('signupview', views.signup_view, name='signupview'),
    path('signout', views.signout, name='signout'),
    path('signinuser', views.signin_user, name='signinuser'),
    path('cartview', views.cart_view, name='cartview'),
    path('registrationview', views.registration_view, name='registrationview'),
    path('registeritem', views.register_item, name='registeritem'),
    path('edititemview', views.edititem_view, name='edititemview'),
    path('edititem/<int:item_id>',
          views.edit_item, name='edititem'),
    path('updateitem/<int:item_id>', views.update_item,
          name='updateitem'),
    path('deleteitem/<int:item_id>', views.delete_item,
          name='deleteitem'),
    ...........................................................................

    ...........................................................................
    path('addcart/<int:item_id>', views.add_to_cart,
          name='addcart'),
    path('removeitem/<int:item_id>', views.remove_from_cart,
          name='removeitem'),
    path('paymentview/', views.payment_view,
          name='paymentview'),
    path('purchaseitems/', views.purchase_items,
          name='purchaseitems'),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

加えられた行

added rows

追加された機能は上から、カートに商品を加える、商品をカートから削除する、支払いページに飛ぶ、支払いをするです。

From the top, you can add items to your cart, remove items from your cart, go to the payment page, and purchase selected items.

addcartとremoveitemは個別の商品に対して行う操作なので、それぞれ引数に商品のIDが渡されます。

Because addcart and removeitem are operations that are performed on individual items, the argument is passed the ID of the item.


Responsive image




② viewsとtemplatetags
② views and templatetags



続いてviews.pyです。

Next is views.py.


demoshop/views.py

from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render, redirect, reverse, get_object_or_404
from urllib.parse import urlencode, unquote
from django.contrib.auth.models import User, Group
from demoshop.models import (Item, Customer, Seller,
                                Transaction)
from django.http import HttpResponse

# Create your views here.


def index(request):
    if request.method == "GET":
        if request.user.is_authenticated:
            return redirect('demoshop:home')
        else:
            if request.method == "GET":
                items = Item.objects.all()
                return render(request, 'demoshop/index.html',
                                {'items': items})
    else:
        HttpResponse(status=500)


def home(request):
    if request.method == "GET":
        user = request.user
        if request.user.is_authenticated:
            username = user.username
            if request.user.groups.filter(name="sellers").count() != 0:
                is_seller = True
                items = Item.objects.filter(seller=user.seller.id)
                context = {'message': f"ようこそ、{username}さん",
                            'items': items, 'isSeller': is_seller}
                return render(request, 'demoshop/home.html', context)

            else:
                is_seller = False
                items = Item.objects.all()
                transactions = Transaction.objects.filter(
                    customer=user.customer.id)
                added_list = []
                if transactions.count() != 0:
                    for item in items:
                        added = transactions.filter(item=item.id)
                        if added.count() != 0:
                            added_list.append(added[0].added_to_a_cart)
                        else:
                            added_list.append(False)
                else:
                    for i in range(items.count()):
                        added_list.append(False)

                context = {'message': f"ようこそ、{username}さん",
                            'items': items, 'isSeller': is_seller,
                            "addedlist": added_list}

                return render(request, 'demoshop/home.html', context)
        return redirect('demoshop:index')
    else:
        return HttpResponse(status=500)


def add_to_cart(request, item_id):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return redirect('demoshop:signin')
        item = get_object_or_404(Item, pk=item_id)
        transaction = Transaction.objects.filter(
            customer=user.customer.id, item=item.id)
        if transaction.count() != 0:
            return render(
                request, 'demoshop:home', {
                    "error": "Item already is in your cart"})

        Transaction.objects.create(
            customer=user.customer, item=item,
            added_to_a_cart=True)

        return redirect('demoshop:cartview')
    else:
        return HttpResponse(status=500)


def remove_from_cart(request, item_id):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return HttpResponse(status=500)
        item = get_object_or_404(Item, pk=item_id)
        transaction = Transaction.objects.filter(
            customer=user.customer.id, item=item.id)
        if transaction[0].customer.user_id == user.id:
            item = transaction[0].item
            is_removed = 'removed'
            context = {'message': "商品カゴから削除されました",
                        'title': item.title, 'description': item.description,
                        'price': item.price, 'imageFile': item.image,
                        'stock': item.stock,
                        'status': is_removed, 'isSeller': False}
            Transaction.objects.get(
                customer=user.customer.id, item=item.id).delete()
            return render(request, 'demoshop/cartview.html', context)
        else:
            return HttpResponse(status=500)
    else:
        return redirect('demoshop:cartview')


def payment_view(request):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return redirect('demoshop:signin')
        item_ids = request.POST.getlist('inputItemID')
        select_nums = request.POST.getlist('inputSelectNum')
        if item_ids and select_nums:
            total_amount_paid = 0.0
            cart_items = []
            for i, item_id in enumerate(item_ids):
                item_id = int(item_id)
                select_num = int(select_nums[i])
                item = get_object_or_404(Item, pk=item_id)
                if item.stock - select_num < 0:
                    return render(request, 'demoshop/paymentview.html',
                                    {'error_message': '<span>Something is wrong.</span> \
                            <br><span>Please go back and try again.</span>',
                                    'isSeller': False})
                transaction = Transaction.objects.filter(
                    customer=user.customer.id, item=item.id)[0]
                total_amount_paid += select_num * item.price
                transaction.select_num = select_num
                transaction.save()
                cart_items.append(transaction.item)
            return render(request, 'demoshop/paymentview.html', {
                'message': "支払いをする",
                'totalAmountPaid': total_amount_paid,
                'cart_items': cart_items,
                'select_nums': select_nums,
                'isSeller': False})
        else:
            return render(request, 'demoshop/paymentview.html',
                            {'error_message': '<span>Something is wrong.</span> \
                            <br><span>Please go back and try again.</span>',
                            'isSeller': False})
    else:
        return redirect('demoshop:cartview')


def purchase_items(request):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return redirect('demoshop:signin')
        address = request.POST.get('inputAddress')
        method = request.POST.get('inputMethod')
        if not address and not method:
            return render(request, 'demoshop/paymentview.html',
                            {'error_message': '登録項目は全て入力してください。',
                            'isSeller': True})
        item_ids = request.POST.getlist('inputItemID')
        total_amount_paid = 0.0
        for i, item_id in enumerate(item_ids):
            item_id = int(item_id)
            item = get_object_or_404(Item, pk=item_id)
            transaction = Transaction.objects.filter(
                customer=user.customer.id, item=item.id)[0]
            if item.stock - transaction.select_num >= 0:
                total_amount_paid += transaction.select_num * item.price
                item.stock = item.stock - transaction.select_num
                if item.stock == 0:
                    item.available = False
                item.save()
                Transaction.objects.filter(
                    customer=user.customer.id, item=item.id).delete()
            else:
                return render(request, 'demoshop/paymentview.html',
                                {'error_message': '<span>Something is wrong.</span> \
                        <br><span>Please go back and try again.</span>',
                                'isSeller': False})
        return render(request, 'demoshop/paymentview.html',
                        {'amount': total_amount_paid, 'address': address,
                        'method': method, 'name': user.username,
                        'email': user.email, 'checkout': True,
                        'message': '支払いは完了しました', 'isSeller': False})
    else:
        return HttpResponse(status=500)


def cart_view(request):
    if request.method == "GET":
        user = request.user
        if request.user.is_authenticated:
            if user.groups.filter(name="sellers").count() != 0:
                return redirect('demoshop:home')
            else:
                transactions = Transaction.objects.filter(
                    customer=user.customer.id)

                if transactions.count() != 0:
                    is_cart = True
                else:
                    is_cart = False

                cart_items = []
                for transaction in transactions:
                    cart_items.append(transaction.item)

                return render(request, 'demoshop/cartview.html', {
                    'message': "商品カゴ", 'cart_items': cart_items,
                    'user_id': user.id, 'isCart': is_cart, 'isSeller': False})
        else:
            return redirect('demoshop:index')
    else:
        return HttpResponse(status=500)


def registration_view(request):
    if request.method == "GET":
        if request.user.is_authenticated:
            if request.user.groups.filter(name="sellers").count() != 0:
                check = request.GET.get('check')
                if check:
                    title = request.GET.get('title')
                    description = request.GET.get('description')
                    price = request.GET.get('price')
                    stock = request.GET.get('stock')
                    try:
                        image_file_url = '/media/' + \
                            unquote(request.GET.get('imageFile'))
                    except BaseException:
                        return render(request,
                                        'demoshop/registrationview.html',
                                        {'message': "商品を登録する",
                                        'isSeller': True})

                    context = {'title': title, 'description': description,
                                'isSeller': True,
                                'price': price, 'check': check,
                                'imageFileURL': image_file_url,
                                'stock': stock, 'message': "商品は登録されました"}
                    return render(
                        request, 'demoshop/registrationview.html', context)
                else:
                    return render(request, 'demoshop/registrationview.html', {
                        'message': "商品を登録する", 'isSeller': True,
                        'optionNumbers': list(range(1, 100))})
            else:
                return redirect('demoshop:home')
        else:
            return redirect('demoshop:index')
    else:
        return HttpResponse(status=500)


def register_item(request):
    if request.method == "POST":
        user = request.user
        title = request.POST['inputTitle']
        description = request.POST['inputDescription']
        price = request.POST['inputPrice']
        imageFile = request.FILES['inputImage']

        # add
        stock = request.POST['inputStock']

        seller = user.seller

        if not title and not description and not price and not imageFile:
            return render(request,
                            'demoshop/registrationview.html',
                            {'error_message': '商品の登録項目は全て入力してください。',
                            'isSeller': True})
        try:
            float_price = float(price)
        except BaseException:
            return render(request, 'demoshop/registrationview.html',
                            {'error_message': '値段が数ではありません。', 'isSeller': True})

        if float_price <= 0:
            return render(request, 'demoshop/registrationview.html',
                            {'error_message': '0以下の値段は登録できません。',
                            'isSeller': True})
        if float_price >= 1000000:
            return render(request,
                            'demoshop/registrationview.html',
                            {'error_message': '100万円以上の値段は登録できません。',
                            'isSeller': True})

        if len(title) > 30:
            length = f"{title}: {len(title)}文字"
            return render(request, 'demoshop/registrationview.html',
                            {'error_message': '30文字を超えるタイトルは登録できません。',
                            'supplement': length, 'isSeller': True})
        if len(description) > 90:
            length = f"{description}: {len(description)}文字"
            return render(request, 'demoshop/registrationview.html',
                            {'error_message': '90文字を超える商品説明は登録できません。',
                            'supplement': length, 'isSeller': True})

        ext = imageFile.name.split('.')[-1]
        if not (ext == 'jpg' or ext == 'JPG' or ext == 'png' or ext == 'PNG'):
            return render(request,
                            'demoshop/registrationview.html',
                            {'error_message': '画像ファイルはjpgかpngにしてください。',
                            'supplement': f"{imageFile.name} : {ext}",
                            'isSeller': True})

        inventory = Item.objects.filter(seller=seller)
        if inventory is not None:
            for item in inventory:
                if title == item.title:
                    return render(
                        request,
                        'demoshop/registrationview.html',
                        {
                            'error_message': 'The same product name cannot be registered.',
                            'supplement': f"{title} has already been registered.",
                            'isSeller': True})

        try:
            redirect_url = reverse('demoshop:registrationview')
            registered = True

            item = Item.objects.create(
                title=title,
                description=description,
                price=price,
                image=imageFile,
                seller=seller,
                stock=stock)

            parameters = urlencode({'title': item.title,
                                    'description': item.description,
                                    'price': item.price,
                                    'check': registered,
                                    'stock': item.stock,
                                    'imageFile': item.image})
            url = f'{redirect_url}?{parameters}'
        except BaseException:
            return render(request,
                            'demoshop/registrationview.html',
                            {'error_message': 'Something is wrong. Please go back and try again.',
                            'isSeller': True})

        return redirect(url)
    else:
        return render(request,
                        'demoshop/registrationview.html',
                        {'error_message': 'Something is wrong. Please go back and try again.',
                        'isSeller': True})


def edititem_view(request):
    if request.method == "GET":
        if request.user.is_authenticated:
            user = request.user
            if request.user.groups.filter(name="sellers").count() != 0:
                items = Item.objects.filter(seller=user.seller.id)
                context = {'message': "商品を編集する",
                            'items': items, 'isSeller': True}
                return render(request, 'demoshop/edititemview.html', context)
            else:
                return redirect('demoshop:home')
        return redirect('demoshop:index')
    else:
        return HttpResponse(status=500)


def edit_item(request, item_id):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return HttpResponse(status=500)
        if request.user.groups.filter(name="sellers").count() == 0:
            return HttpResponse(status=500)
        item = get_object_or_404(Item, pk=item_id)
        if item.seller.user_id == user.id:
            title = request.POST['inputTitle']
            description = request.POST['inputDescription']
            price = request.POST['inputPrice']
            imageFile = request.FILES['inputImage']
            stock = request.POST['inputStock']

            if not title and not description and not price and not imageFile:
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '商品の登録項目は全て入力してください。',
                                'isSeller': True})

            inventory = Item.objects.filter(seller=user.seller)
            if inventory is not None:
                for i in inventory:
                    if title == i.title:
                        return render(
                            request,
                            'demoshop/edititemview.html',
                            {
                                'error_message': 'The same product name cannot be registered.',
                                'supplement': f"{title} has already been registered.",
                                'isSeller': True})

            item.title = title

            try:
                float_price = float(price)
            except BaseException:
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '値段が数ではありません。',
                                'isSeller': True})

            if float_price <= 0:
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '0以下の値段は登録できません。',
                                'isSeller': True})
            if float_price >= 1000000:
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '100万円以上の値段は登録できません。',
                                'isSeller': True})

            if len(title) > 30:
                length = f"{title}: {len(title)}文字"
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '30文字を超えるタイトルは登録できません。',
                                'supplement': length, 'isSeller': True})
            if len(description) > 90:
                length = f"{description}: {len(description)}文字"
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '90文字を超える商品説明は登録できません。',
                                'supplement': length, 'isSeller': True})

            item.description = description
            item.price = price

            ext = imageFile.name.split('.')[-1]
            if not (ext == 'jpg' or ext == 'JPG' or ext ==
                    'png' or ext == 'PNG'):
                return render(request, 'demoshop/edititemview.html',
                                {'error_message': '画像ファイルはjpgかpngにしてください。',
                                'supplement': f"{imageFile.name} : {ext}",
                                'isSeller': True})

            try:
                if item.image:
                    item.image.delete(save=False)
                item.image = imageFile
            except BaseException:
                return render(
                    request,
                    'demoshop/edititemview.html',
                    {
                        'error_message': 'Something is wrong. Please go back and try again.',
                        'isSeller': True})

            is_updated = 'updated'
            is_seller = True

            if int(stock) >= 0:
                item.stock = stock
                item.available = True
                item.save()
            else:
                context = ({'message': "The number of items must be at least one",
                            'isSeller': is_seller, 'status': is_updated})
                return render(request, 'demoshop/edititemview.html', context)

            context = ({'message': "商品は更新されました",
                        'title': item.title,
                        'description': item.description,
                        'price': item.price,
                        'imageFile': item.image,
                        'isSeller': is_seller,
                        'status': is_updated,
                        'stock': stock})

            return render(request, 'demoshop/edititemview.html', context)
        else:
            return HttpResponse(status=500)
    else:
        return redirect('demoshop:edititemview')


def update_item(request, item_id):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return HttpResponse(status=500)
        if request.user.groups.filter(name="sellers").count() == 0:
            return HttpResponse(status=500)
        item = get_object_or_404(Item, pk=item_id)
        if item.seller.user_id == user.id:
            is_update = 'update'
            is_seller = True
            # add
            context = {
                'message': "商品情報を更新します",
                'item': item,
                'isSeller': is_seller,
                'status': is_update,
                'optionNumbers': list(
                    range(
                        1,
                        100))}
            return render(request, 'demoshop/edititemview.html', context)
        else:
            return HttpResponse(status=500)
    else:
        return redirect('demoshop:edititemview')


def delete_item(request, item_id):
    if request.method == "POST":
        user = request.user
        if not user.is_authenticated:
            return HttpResponse(status=500)
        if request.user.groups.filter(name="sellers").count() == 0:
            return HttpResponse(status=500)
        item = get_object_or_404(Item, pk=item_id)
        if item.seller.user_id == user.id:
            is_deleted = 'deleted'
            is_seller = True
            context = {'message': "商品は削除されました",
                        'title': item.title, 'description': item.description,
                        'price': item.price,
                        'isSeller': is_seller, 'status': is_deleted,
                        'stock': item.stock}
            Item.objects.get(pk=item_id).delete()
            return render(request, 'demoshop/edititemview.html', context)
        else:
            return HttpResponse(status=500)
    else:
        return redirect('demoshop:edititemview')


def signin(request):
    if request.user.is_authenticated:
        return redirect('demoshop:home')
    return render(request, 'demoshop/signin.html')


def signup_view(request):
    if request.user.is_authenticated:
        return redirect('demoshop:home')
    return render(request, 'demoshop/signupview.html')


def signout(request):
    logout(request)
    return render(request, 'demoshop/signin.html',
                    {'error_message': "サインアウトしました"})


def signin_user(request):
    if request.method == "POST":
        username = request.POST['inputUsername']
        password = request.POST['inputPassword']

        user = authenticate(request,
                            username=username,
                            password=password)

        if user is not None:
            login(request, user)
            return redirect('demoshop:home')
        else:
            return render(request, "demoshop/signin.html",
                            {'error_message':
                            "ユーザー名かパスワードが間違っています!"})
    else:
        return redirect('demoshop:index')


def signup(request):
    if request.method == "POST":
        username = request.POST['inputUsername']
        email = request.POST['inputEmail']
        password = request.POST['inputPassword']
        seller = False

        try:
            if request.POST['seller']:
                seller = True
        except KeyError:
            seller = False
        if username is not None and \
                email is not None and \
                password is not None:
            if User.objects.filter(username=username).exists():
                return render(request, 'demoshop/registration.html',
                                {'error_message':
                                f"このユーザー名({username})\
                                    はすでに使われています"})
            elif User.objects.filter(email=email).exists():
                return render(request,
                                'demoshop/registration.html',
                                {'error_message': f"このメール \
                                    ({email})はすでに使われています"})
            user = User.objects.create_user(username, email, password)

            if seller:
                if Group.objects.filter(name='sellers').exists():
                    seller_group = Group.objects.get(name='sellers')
                else:
                    Group.objects.create(name='sellers').save()
                    seller_group = Group.objects.get(name='sellers')
                seller_group.user_set.add(user)
                Seller.objects.create(user=user).save()
            else:
                Customer.objects.create(user=user).save()
            user.save()
            login(request, user)
        return redirect('demoshop:home')
    else:
        return redirect('demoshop:registrationview')

add_to_cart関数はPOSTリクエストとitemのidを受けたら、それが既にTransactionモデルとして作られているかを判定します。

When the add_to_cart function receives the POST request and the id of the item, it determines whether it is already created as a Transaction model.

モデルがないなら、新しいTransactionモデルを作成する。モデルがあるならカートビューに表示されているのでTransactionモデルを作らない。

If you don't have a model, create a new Transaction model. If there is a model, it is displayed in the cart view, so I don't make a Transaction model.


Responsive image

remove_from_cart関数はPOSTリクエストと商品のidを受けたら、そのIDの商品のTransactionモデルを消去します。(Itemモデル自体は削除されないのがポイントで、顧客に紐づいている商品(Transaction)が削除される)

When the remove_from_cart function receives the POST request and the item id, it removes the Transaction model for the item with that ID. (The point is that the Item model itself is not deleted, and the item (Transaction) linked to the customer is deleted.)


Responsive image

payment_view関数は「支払い」ボタンが押された時に、カートに入っている商品すべてのIDと選択された数をリストとして受け取り「支払いページ」へその情報を渡します。

When the [Payment] button is pressed, the payment_view function takes the IDs of all the items in the cart and the selected number as a list and passes that information to [Payment Page].

そしてpurchase_items関数内で「購入」ボタンが押された時に、支払いをする商品の確認一覧リストから商品IDと、 payment_view関数内で各商品毎のTransactionモデルに保存された選択数を元に支払い処理を行います。

Then, when the [Purchase] button is pressed in the purchase_items function, the item ID is obtained from the confirmation list of the item to be paid displayed on the page, and Payment processing is performed based on the number of choices for each product stored in each Transaction model in the payment_view function.


Responsive image

補足ですが、[payment_view関数]と[purchase_items関数]内で使われているitemのIDはHTML内のinputタグから取得していますが、 取引が完了しないとItemモデルの変更を保存できないので、inputタグをhiddenにしてそこからitemのIDをPOSTリクエストで取得しています。 (このようにするとブラウザには表示されない、 しかしHTMLソースとしては確認できるのでセキリティ上あまりよろしくない)

In addition, the ID of item used in [payment_view function] and [purchase_items function] is obtained from the input tag in HTML. Since I cannot save changes to the Item model until the transaction is completed, I set the input tag to hidden and get the ID of the item in the POST request. (This will not show up in the browser, but it will show up as an HTML source, which is not very good for security.)

<input type="hidden" name="inputItemID" value="{{ item.id }}">

注意点: 今回は取引完了後Transactionモデルを削除しています。当たり前ですが、取引記録を残しておくべきなので、遊びではなく商用としてサイトを作る場合は「取引記録用の帳簿モデル」を作成する必要があります。

Note: I have removed the Transaction model after the transaction is completed. Of course, you should keep a transaction record, so if you want to create a site not for fun but for business, you need to create a "book model for transaction records".

Transaction.objects.filter(customer=user.customer.id, item=item.id).delete()


カスタムテンプレートフィルタ
Custom Template Filter

テンプレート内で使用するテンプレートフィルタを作成したので、使い方を説明しておきます。

Now that I've created my own template filters for use in templates, so I'll show you how to use them.

まず「アプリフォルダ」直下に「templatetags」という名前のフォルダを作成し、その中に「__init__.py」という空のファイルを作成します。

Create a folder named 「templatetags」 directly under 「App Folder」 and create an empty file named "__init__. py" in it.

それから「indices.py」と「get_range.py」という二つのファイルを以下の様に作成します。

Then create two files called [indices.py] and [get_range.py] as follows.

your django project
    - settings.py
    - urls.py
    - ......
your django application
    - views.py
    - urls.py
    - .........
    - templatetags
        - __init__.py
        - indices.py
        - get_range.py
    - .........
    - .........
demoshop/templatetags/indices.py
from django import template
register = template.Library()


@register.filter
def indices(members, i):
    return members[int(i)]
demoshop/templatetags/get_range.py
from django import template
register = template.Library()


@register.filter
def get_range(num_of_items):
    return range(1, num_of_items + 1)

[indices関数]はテンプレート内でViewからリストを渡した時にリストの中身をインデックス番号で参照することが簡単にできるようにします。

[indices function] makes it easy to refer to the contents of a list by index number when passing it from View to Template.

[get_range関数]は渡された数からrange関数をの結果を返します。上で説明したindices関数と同様にDjangoのTemplateだとこれらの表現が難しいので、カスタムテンプレートフィルタとして作成しなければなりませんでした。

[get_range function] returns the result of the range function from the number passed in. Like the indices function described above, Django's Tempate is difficult to represent these method, so I had to create it as a custom template filter.

使う時はstatic tagと同様にHTML内で以下のようにtagを呼び出して使用します。

When used, the tag can be called in HTML like a static tag.

home.html
{% if not isSeller %}
{% if item.available %}
{% load indices %}
{% if addedlist|indices:forloop.counter0 %}
<div class="card-body text-center">
    <p class="card-text text-danger">Added to a cart</p>
</div>
{% else %}
cartview.html
<select class="form-control" name="inputSelectNum" required>
    {% load get_range %}
    {% for n in item.stock|get_range %}
    <option>{{ n }}</option>
    {% endfor %}
</select>



③ templates
③ templates



続いてテンプレートのHTMLです。

Next is HTML in templates.


base.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
  <meta name="generator" content="Jekyll v4.0.1">
  <title>デモショップ</title>
  {% load static %}
  <!-- Bootstrap core CSS -->
  <link href="{% static 'demoshop/css/bootstrap.css' %}" rel="stylesheet">

  <style>
    .bd-placeholder-img {
      font-size: 1.125rem;
      text-anchor: middle;
      -webkit-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
    }

    @media (min-width: 768px) {
      .bd-placeholder-img-lg {
        font-size: 3.5rem;
      }
    }
  </style>
  <!-- Custom styles for this template -->
  {% block maincss %}{% endblock maincss %}
  {% block signincss %}{% endblock signincss %}

</head>

<body>
  <header>
    <div class="collapse bg-dark" id="navbarHeader">
      <div class="container">
        <div class="row">
          <div class="col-sm-8 col-md-7 py-4">
            <h4 class="text-white">デモショップ</h4>
          </div>
          <div class="col-sm-4 offset-md-1 py-4">
            <h4 class="text-white">Menu</h4>
            <ul class="list-unstyled">

              {% if isSeller is True %}
              <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</a></li>
              <li><a href="{% url 'demoshop:registrationview' %}" style="color: coral;">Register Your
                  Product</a></li>
              <li><a href="{% url 'demoshop:edititemview' %}" style="color: coral;">Update or Delete</a></li>
              <li><a href="{% url 'demoshop:home' %}" class="text-white">Home</a></li>

              {% elif isSeller is False %}
              <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</a></li>
              <li><a href="{% url 'demoshop:cartview' %}" style="color: coral;">Cart</a></li>
              <li><a href="{% url 'demoshop:home' %}" class="text-white">Home</a></li>

              {% else %}
              <li><a href="{% url 'demoshop:signup' %}" class="text-white">Sign up</a></li>
              <li><a href="{% url 'demoshop:signin' %}" class="text-white">Sign in</a></li>
              <li><a href="{% url 'demoshop:index' %}" style="color: coral;">Shop</a></li>

              {% endif %}

            </ul>
          </div>
        </div>
      </div>
    </div>
    <div class="navbar navbar-dark bg-dark shadow-sm">
      <div class="container d-flex justify-content-between">
        <a href="{% url 'demoshop:index' %}" class="navbar-brand d-flex align-items-center">
          <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
            stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="mr-2"
            viewBox="0 0 24 24" focusable="false">
            <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
            <circle cx="12" cy="13" r="4" /></svg>
          <strong>デモショップ</strong>
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader"
          aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
      </div>
    </div>
  </header>

  <script src="{% static 'demoshop/js/jquery-3.5.1.slim.min.js' %}"></script>
  <script src="{% static 'demoshop/js/bootstrap.bundle.js' %}"></script>

  <main role="main">
    {% block indexcontent %}{% endblock indexcontent %}
    {% block signincontent %}{% endblock signincontent %}
    {% block signupviewcontent %}{% endblock signupviewcontent %}
    {% block homecontent %}{% endblock homecontent %}
    {% block cartviewcontent %}{% endblock cartviewcontent %}
    {% block edititemviewcontent %}{% endblock edititemviewcontent %}
    {% block registrationviewcontent %}{% endblock registrationviewcontent %}
    {% block paymentviewcontent %}{% endblock paymentviewcontent %}
  </main>


</body>

</html>


home.html

{% extends "demoshop/base.html" %}
{% load static %}
{% block maincss %}
<link href="{% static 'demoshop/css/album.css' %}" rel="stylesheet">{% endblock maincss %}

{% block homecontent %}
{% load humanize %}
<section class="jumbotron text-center">
    {% if isSeller %}
    <div class="container">
        <h2>{{ message }}<br><span style="color: coral;">(Seller)</span></h2>
        <br>
        <h1>Inventory</h1>
    </div>
    {% else %}
    <div class="container">
        <!-- add -->
        {% if error %}
        <h2 class="text-danger">{{ error }}</h2>
        {% else %}
        <h2>{{ message }}</h2>
        {% endif %}
        <!-- add -->
    </div>
    {% endif %}
</section>

<div class="album py-5 bg-light">
    <div class="container">
        <div class="row">
            {% for item in items %}
            <div class="col-md-6">
                <div class="card mb-6 shadow-sm">
                    {% if item.available is False %}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">non-tradable</text>
                    </svg>
                    {% elif item.image.url == "/media/product_images/000/000.png" %}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">Thumbnail</text>
                    </svg>
                    {% else %}
                    <div class="card-img-top">
                        <img width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    </div>
                    {% endif %}
                    <div class="card-body">
                        <h5 class="card-title">Title</h5>
                        <p class="card-text">{{ item.title }}</p>
                    </div>
                    <div class="card-body overflow-auto" style="height: 100px;">
                        <h5 class="card-title">Description</h5>
                        <p class="card-text">{{ item.description }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Price</h5>
                        <p class="card-text">¥{{ item.price|floatformat|intcomma }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Stock</h5>
                        <p class="card-text">{{ item.stock }}</p>
                    </div>
                    <!-- add and indices customtag -->
                    {% if not isSeller %}
                    {% if item.available %}
                    {% load indices %}
                    {% if addedlist|indices:forloop.counter0 %}
                    <div class="card-body text-center">
                        <p class="card-text text-danger">Added to a cart</p>
                    </div>
                    {% else %}
                    <div class="text-center" style="padding-bottom: 1rem;">
                        <form class="form-signin" action="{% url 'demoshop:addcart' item.id %}" method="post">
                            {% csrf_token %}
                            <div class="form-group" style="margin: 1rem 3rem;">
                                <input type="submit" class="btn btn-primary" value="Add to Cart" />
                            </div>
                        </form>
                    </div>
                    {% endif %}
                    {% else %}
                    <div class="card-body text-center">
                        <h5 class="card-text">Not available</h5>
                    </div>
                    {% endif %}
                    {% else %}
                    {% if item.available %}
                    <div class="card-body text-center">
                        <p class="card-text text-primary">Available</p>
                    </div>
                    {% else %}
                    <div class="card-body text-center">
                        <p class="card-text text-danger">Not available</p>
                    </div>
                    {% endif %}
                    {% endif %}
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</div>

<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock homecontent %}


index.html

{% extends "demoshop/base.html"%}
{% load static%}
{% block maincss%}
<link href="{% static 'demoshop/css/album.css'%}" rel="stylesheet">{% endblock maincss%}

{% block indexcontent%}
{% load humanize%}
<section class="jumbotron text-center">
    <div class="container">
        <h1>デモショップ</h1>
    </div>
</section>

<div class="album py-5 bg-light">
    <div class="container">
        <div class="row">
            {% for item in items%}
            <div class="col-md-6">
                <div class="card mb-6 shadow-sm">
                    {% if item.available is False%}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">non-tradable</text>
                    </svg>
                    {% elif item.image.url == "/media/product_images/000/000.png"%}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">Thumbnail</text>
                    </svg>
                    {% else%}
                    <div class="card-img-top">
                        <img width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    </div>
                    {% endif%}
                    <div class="card-body">
                        <h5 class="card-title">Title</h5>
                        <p class="card-text">{{ item.title }}</p>
                    </div>
                    <div class="card-body overflow-auto" style="height: 100px;">
                        <h5 class="card-title">Description</h5>
                        <p class="card-text">{{ item.description }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Price</h5>
                        <p class="card-text">¥{{ item.price|floatformat|intcomma }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Stock</h5>
                        <p class="card-text">{{ item.stock }}</p>
                    </div>
                    {% if item.available is False%}
                    <div class="card-body text-center">
                        <h5 class="card-text">Not available</h5>
                    </div>
                    {% else%}
                    <div class="text-center" style="padding-bottom: 1rem;">
                        <a href="{% url 'demoshop:signin'%}" class="btn btn-primary" role="button"
                            aria-pressed="true">Add to Cart</a>
                    </div>
                    {% endif%}
                </div>
            </div>
            {% endfor%}
        </div>
    </div>
</div>

<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock indexcontent%}


edititemview.html

{% extends "demoshop/base.html" %}
{% load static %}
{% block maincss %}
<link href="{% static 'demoshop/css/album.css' %}" rel="stylesheet">{% endblock maincss %}

{% block edititemviewcontent %}
{% load humanize %}
<section class="jumbotron text-center">
    <div class="container">
        <h1>{{ message }}</h1>
        <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal" style="color: red;">{{ error_message }}</h1>
        <p style="color: red;">{{ supplement }}</p>
    </div>
</section>

{% if status == 'deleted' %}
<section class="jumbotron text-center">
    <div class="album py-5 bg-light">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card mb-12 shadow-sm">
                        <div class="card-img-top">
                            <h5>画像も削除されました</h5>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Title</h5>
                            <p class="card-text">{{ title }}</p>
                        </div>
                        <div class="card-body overflow-auto" style="height: 100px;">
                            <h5 class="card-title">Description</h5>
                            <p class="card-text">{{ description }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Price</h5>
                            <p class="card-text">¥{{ price|floatformat|intcomma }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Stock</h5>
                            <p class="card-text">{{ stock }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
{% elif status == 'update' %}
<div class="text-center">
    <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal">Update Your Product</h1>
    <form action="{% url 'demoshop:edititem' item.id %}" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputTitle" class="sr-only">Title</label>
            <input type="text" name="inputTitle" id="inputTitle" class="form-control" placeholder="{{ item.title }}"
                required autofocus>
        </div>
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputDescription" class="sr-only">Description</label>
            <textarea name="inputDescription" id="inputDescription" class="form-control"
                placeholder="{{ item.description }}" rows="3" required autofocus></textarea>
        </div>
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputEmail" class="sr-only">Price</label>
            <input type="number" name="inputPrice" id="inputPrice" class="form-control" placeholder="{{ item.price }}"
                required autofocus>
        </div>

        <!-- upload image file -->
        <div class="form-group" style="margin: 3rem 3rem;">
            <label for="inputImage">Upload your product images</label>
            <img class="mx-auto d-block" width="24%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
            <div class="file-upload-wrapper">
                <input type="file" id="input-file-now" class="file-upload" name="inputImage" placeholder="Image"
                    required />
            </div>
        </div>

        <!-- add option -->
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputStock">Number of Stocks</label>
            <select class="form-control" name="inputStock" id="inputStock" required>
                {% for n in optionNumbers %}
                <option>{{ n }}</option>
                {% endfor %}
            </select>
        </div>

        <!-- Available this item? -->
        {% if item.stock <= 0 %}
        <div class="form-group" style="margin: 3rem 3rem;">
            <div class="checkbox mb-3">
                <label>
                    <span style="color: red; margin-left : 10px;">Available this item?</span>
                    <br>
                    <span>If the number of stock is zero, you cannot check it.</span>
                    <br>
                    <span> You must have at least one in stock to make it available again.</span>
                </label>
            </div>
        </div>
        {% endif %}

        <div class="form-group" style="margin: 1rem 3rem;">
            <button class="btn btn-primary btn-block" type="submit" value="Edit">Save</button>
        </div>
    </form>
</div>
{% elif status == 'updated' %}
<section class="jumbotron text-center">
    <div class="album py-5 bg-light">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card mb-12 shadow-sm">
                        <div class="card-img-top">
                            <img width="100%" height="225" src="{{ imageFile.url }}" alt="Thumbnail">
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Title</h5>
                            <p class="card-text">{{ title }}</p>
                        </div>
                        <div class="card-body overflow-auto" style="height: 100px;">
                            <h5 class="card-title">Description</h5>
                            <p class="card-text">{{ description }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Price</h5>
                            <p class="card-text">¥{{ price|floatformat|intcomma }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Stock</h5>
                            <p class="card-text">{{ stock }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
{% else %}
<div class="album py-5 bg-light">
    <div class="container">
        <div class="row">
            {% for item in items %}
            <div class="col-md-6">
                <div class="card mb-6 shadow-sm">
                    {% if item.available is False %}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">non-tradable</text>
                    </svg>
                    {% elif item.image.url == "/media/product_images/000/000.png" %}
                    <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                        xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                        role="img" aria-label="Placeholder: Thumbnail">
                        <title>Placeholder</title>
                        <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                            dy=".3em">Thumbnail</text>
                    </svg>
                    {% else %}
                    <div class="card-img-top">
                        <img width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    </div>
                    {% endif %}
                    <div class="card-body">
                        <h5 class="card-title">Title</h5>
                        <p class="card-text">{{ item.title }}</p>
                    </div>
                    <div class="card-body overflow-auto" style="height: 100px;">
                        <h5 class="card-title">Description</h5>
                        <p class="card-text">{{ item.description }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Price</h5>
                        <p class="card-text">¥{{ item.price|floatformat|intcomma }}</p>
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Stock</h5>
                        <p class="card-text">{{ item.stock }}</p>
                    </div>
                    <div class="text-center" style="padding-bottom: 1rem;">
                        <form class="form-signin" action="{% url 'demoshop:updateitem' item.id %}" method="post">
                            {% csrf_token %}
                            <div class="form-group" style="margin: 1rem 3rem;">
                                <input type="submit" class="btn btn-info" value="Update" />
                            </div>
                        </form>
                        <button type="button" class="btn btn-danger" data-toggle="modal"
                            data-deleteurl="{% url 'demoshop:deleteitem' item.id %}"
                            data-target="#deleteModal">Delete</button>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</div>
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle"
    aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLongTitle">Delete the selected item</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p>You cannot undo the deletion.</p>
                <p>Are you sure you want to delete it?</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                <form id="delete-form" action="" method="POST">
                    {% csrf_token %}
                    <button type="submit" class="btn btn-danger" id="deletebutton">Delete</button>
                </form>
            </div>
        </div>
    </div>
</div>
<script>
    $("#deleteModal").on("show.bs.modal", function (e) {
        var button = $(e.relatedTarget)
        var deleteurl = button.data("deleteurl")

        var modal = $(this)
        $("#delete-form").attr("action", deleteurl);
    })
</script>
{% endif %}

<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock edititemviewcontent %}

registrationview.html

{% extends "demoshop/base.html" %}
{% load static %}
{% block maincss %}
<link href="{% static 'demoshop/css/album.css' %}" rel="stylesheet">{% endblock maincss %}

{% block registrationviewcontent %}
{% load humanize %}
<section class="jumbotron text-center">
    <div class="container">
        <h1>{{ message }}</h1>
        <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal" style="color: red;">{{ error_message }}</h1>
        <p style="color: red;">{{ supplement }}</p>
    </div>
</section>

{% if check %}
<section class="jumbotron text-center">
    <div class="album py-5 bg-light">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card mb-12 shadow-sm">
                        <div class="card-img-top">
                            <img width="100%" height="225" src="{{ imageFileURL }}" alt="Thumbnail">
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Title</h5>
                            <p class="card-text">{{ title }}</p>
                        </div>
                        <div class="card-body overflow-auto" style="height: 100px;">
                            <h5 class="card-title">Description</h5>
                            <p class="card-text">{{ description }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Price</h5>
                            <p class="card-text">¥{{ price|floatformat|intcomma }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Stock</h5>
                            <p class="card-text">{{ stock }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
{% else %}
<div class="text-center">
    <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal">Register Your Product</h1>
    <form action="{% url 'demoshop:registeritem' %}" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputTitle" class="sr-only">Title</label>
            <input type="text" name="inputTitle" id="inputTitle" class="form-control" placeholder="Title" required
                autofocus>
        </div>
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputDescription" class="sr-only">Description</label>
            <textarea name="inputDescription" id="inputDescription" class="form-control" placeholder="Description"
                rows="3" required autofocus></textarea>
        </div>
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputEmail" class="sr-only">Price</label>
            <input type="number" name="inputPrice" id="inputPrice" class="form-control" placeholder="Price" required
                autofocus>
        </div>

        <!-- upload image file -->
        <div class="form-group" style="margin: 3rem 3rem;">
            <label for="inputImage">Upload your product images</label>
            <div class="file-upload-wrapper">
                <input type="file" id="input-file-now" class="file-upload" name="inputImage" placeholder="Image"
                    required />
            </div>
        </div>

        <!-- add option -->
        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputStock">Number of Stocks</label>
            <select class="form-control" name="inputStock" id="inputStock" required>
                {% for n in optionNumbers %}
                <option>{{ n }}</option>
                {% endfor %}
            </select>
        </div>

        <div class="form-group" style="margin: 1rem 3rem;">
            <button class="btn btn-primary btn-block" type="submit" value="Save">Save</button>
        </div>
    </form>
</div>
{% endif %}

<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock registrationviewcontent %}

cartview.html

{% extends "demoshop/base.html" %}
{% load static %}
{% block maincss %}
<link href="{% static 'demoshop/css/album.css' %}" rel="stylesheet">{% endblock maincss %}

{% block cartviewcontent %}
{% load humanize %}
<section class="jumbotron text-center">
    <div class="container">
        <h1>{{ message }}</h1>
    </div>
    <!-- add -->
    {% if status == 'removed' %}
    <div class="album py-5 bg-light">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card mb-12 shadow-sm">
                        {% if imageFile.url == "/media/product_images/000/000.png" %}
                        <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                            xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                            role="img" aria-label="Placeholder: Thumbnail">
                            <title>Placeholder</title>
                            <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                                dy=".3em">Thumbnail</text>
                        </svg>
                        {% else %}
                        <div class="card-img-top">
                            <img width="100%" height="225" src="{{ imageFile.url }}" alt="Thumbnail">
                        </div>
                        {% endif %}
                        <div class="card-body">
                            <h5 class="card-title">Title</h5>
                            <p class="card-text">{{ title }}</p>
                        </div>
                        <div class="card-body overflow-auto" style="height: 100px;">
                            <h5 class="card-title">Description</h5>
                            <p class="card-text">{{ description }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Price</h5>
                            <p class="card-text">¥{{ price|floatformat|intcomma }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Stock</h5>
                            <p class="card-text">{{ stock }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    {% endif %}
</section>


<div class="album py-5 bg-light">
    <div class="container">
        <!-- change -->
        <form action="{% url 'demoshop:paymentview' %}" method="post">
            {% csrf_token %}
            <!-- purchase -->
            {% if isCart %}
            <div class="text-center" style="padding-bottom: 1rem;">
                <button type="submit" class="btn btn-primary btn-lg btn-block">Go to the payment
                    entry field</button>
            </div>
            {% endif %}
            <!-- add -->
            <div class="row">
                {% for item in cart_items %}
                <div class="col-md-6">
                    <div class="card mb-6 shadow-sm">
                        {% if item.available %}
                        {% if item.image.url == "/media/product_images/000/000.png" %}
                        <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                            xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false"
                            role="img" aria-label="Placeholder: Thumbnail">
                            <title>Placeholder</title>
                            <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                                dy=".3em">Thumbnail</text>
                        </svg>
                        {% else %}
                        <div class="card-img-top">
                            <img width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                        </div>
                        {% endif %}
                        <div class="card-body">
                            <h5 class="card-title">Title</h5>
                            <p class="card-text">{{ item.title }}</p>
                        </div>
                        <div class="card-body overflow-auto" style="height: 100px;">
                            <h5 class="card-title">Description</h5>
                            <p class="card-text">{{ item.description }}</p>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Price</h5>
                            <p class="card-text">¥{{ item.price|floatformat|intcomma }}</p>
                        </div>
                        <!-- add form -->
                        <div class="card-body">
                            <h5 class="card-title">Stock</h5>
                            <p class="card-text">{{ item.stock }}</p>
                        </div>
                        <div class="card-body">
                            <p class="card-title">Please decide the number of items you want to buy</p>
                            <input type="hidden" name="inputItemID" value="{{ item.id }}">
                            <select class="form-control" name="inputSelectNum" required>
                                {% load get_range %}
                                {% for n in item.stock|get_range %}
                                <option>{{ n }}</option>
                                {% endfor %}
                            </select>
                        </div>
                        <!-- add -->
                        <div class="text-center" style="padding-bottom: 1rem;">
                            <button type="button" class="btn btn-danger" data-toggle="modal"
                                data-removeurl="{% url 'demoshop:removeitem' item.id %}"
                                data-target="#removeModal">Remove</button>
                        </div>
                        {% endif %}
                    </div>
                </div>
                {% endfor %}
            </div>
        </form>
    </div>
</div>


<!-- remove item from cart -->
<div class="modal fade" id="removeModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle"
    aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLongTitle">Delete the selected item from cart</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p>You cannot undo the deletion.</p>
                <p>Are you sure you want to remove it?</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                <form id="remove-form" action="" method="POST">
                    {% csrf_token %}
                    <button type="submit" class="btn btn-danger" id="removebutton">Remove</button>
                </form>
            </div>
        </div>
    </div>
</div>
<script>
    $("#removeModal").on("show.bs.modal", function (e) {
        var button = $(e.relatedTarget)
        var removeurl = button.data("removeurl")

        var modal = $(this)
        $("#remove-form").attr("action", removeurl);
    })
</script>


<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock cartviewcontent %}

新しく作成したHTMLファイル

Newly created HTML files

paymentview.html

{% extends "demoshop/base.html" %}
{% load static %}
{% block maincss %}
<link href="{% static 'demoshop/css/album.css' %}" rel="stylesheet">{% endblock maincss %}

{% block paymentviewcontent %}
{% load humanize %}
<section class="jumbotron text-center">
    <div class="container">
        <h1>{{ message }}</h1>
        {% if error_message %}
        <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal text-danger">{{ error_message|safe }}</h1>
        {% endif %}
        {% if totalAmountPaid %}
        <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal text-danger">
            ¥{{ totalAmountPaid|floatformat|intcomma }}</h1>
        {% endif %}
        {% if checkout %}
        <div class="album py-5 bg-light">
            <div class="container">
                <div class="row">
                    <div class="col-md-12">
                        <div class="card mb-12 shadow-sm">
                            <div class="card-body" style="height: 100px;">
                                <h5 class="card-title">Amount of payment</h5>
                                <h2 class="card-text text-danger">¥{{ amount|floatformat|intcomma }}</h2>
                            </div>
                            <div class="card-body" style="height: 100px;">
                                <h5 class="card-title">Payment Method</h5>
                                <h3 class="card-text text-danger">{{ method }}</h3>
                            </div>
                            <div class="card-body">
                                <h5 class="card-title">Address</h5>
                                <h3 class="card-text text-danger">{{ address }}</h3>
                            </div>
                            <div class="card-body">
                                <h5 class="card-title">Name</h5>
                                <h3 class="card-text text-danger">{{ name }}</h3>
                            </div>
                            <div class="card-body">
                                <h5 class="card-title">Email</h5>
                                <h3 class="card-text text-danger">{{ email }}</h3>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        {% endif %}
    </div>
</section>

{% if not checkout and not error_message %}
{% load indices %}
<div class="text-center">
    <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal">支払いを完了させる</h1>
    <form action="{% url 'demoshop:purchaseitems' %}" method="post">
        {% csrf_token %}

        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputAddress" class="sr-only">Address</label>
            <input type="text" name="inputAddress" id="inputAddress" class="form-control" placeholder="Address" required
                autofocus>
        </div>

        <div class="form-group" style="margin: 1rem 3rem;">
            <label for="inputMethod">Select your method of payment</label>
            <select class="form-control" name="inputMethod" id="inputMethod" required>
                <option value="bankTransfer">銀行振込</option>
                <option value="creditCard">クレジットカード</option>
                <option value="cashOnDelivery">代引き</option>
            </select>
        </div>


        <div class="album py-5 bg-light">
            <div class="container">
                <h2>商品を確認してください</h2>
                <div class="row">
                    {% for item in cart_items %}
                    <div class="col-md-6">
                        <div class="card mb-6 shadow-sm">
                            {% if item.image.url == "/media/product_images/000/000.png" %}
                            <svg class="bd-placeholder-img card-img-top" width="100%" height="225"
                                xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice"
                                focusable="false" role="img" aria-label="Placeholder: Thumbnail">
                                <title>Placeholder</title>
                                <rect width="100%" height="100%" fill="#55595c" /><text x="50%" y="50%" fill="#eceeef"
                                    dy=".3em">Thumbnail</text>
                            </svg>
                            {% else %}
                            <div class="card-img-top">
                                <img width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                            </div>
                            {% endif %}
                            <div class="card-body">
                                <h5 class="card-title">Title</h5>
                                <p class="card-text">{{ item.title }}</p>
                            </div>
                            <div class="card-body overflow-auto" style="height: 100px;">
                                <h5 class="card-title">Description</h5>
                                <p class="card-text">{{ item.description }}</p>
                            </div>
                            <div class="card-body">
                                <h5 class="card-title">Price</h5>
                                <p class="card-text">¥{{ item.price|floatformat|intcomma }}</p>
                            </div>
                            <div class="card-body">
                                <h5 class="card-title">Number of selected items</h5>
                                <p class="card-text">{{ select_nums|indices:forloop.counter0 }}</p>
                            </div>
                        </div>
                    </div>
                    <input type="hidden" name="inputItemID" value="{{ item.id }}">
                    {% endfor %}
                </div>
            </div>
        </div>

        <div class="text-center">
            <p class="card-text text-danger">Once you have paid, </p>
            <p class="card-text text-danger">You will not be able to cancel this transaction </p>
            <p class="card-text text-danger">Unless you have a specific reason.</p>
            <p class="card-text text-danger">Are you sure you want to purchase it?</p>
        </div>

        <div class="form-group" style="margin: 1rem 3rem;">
            <a href="{% url 'demoshop:cartview' %}" class="btn btn-secondary btn-md" role="button">Cancel</a>
            <button class="btn btn-danger btn-md" type="submit" value="Purchase">Purchase</button>
        </div>
    </form>
</div>
{% endif %}

<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to top</a>
        </p>
        <p>&copy; デモショップ 2020</p>
    </div>
</footer>

{% endblock paymentviewcontent %}

「|safe」は文字列をHTMLとして表示されることができます。 例えばView側で[<p>Hello World</p>]とTemplate側に送るとPタグで囲われた[Hello World]として認識されます。

[|safe] can display strings as HTML. For example, if you send [<p>Hello World</p>] to the template side in View, it will be recognized as [Hello World] surrounded by P tag.



以上です。お疲れ様です。 幼稚な実装内容ですが、色々とカスタマイズすればそれなりのオンラインショップになると思います。 決算システムは[stripe]等の外部サービスを使えばいいです。 Djnagoの組み込みライブラリを活用したり、色々と工夫をしてみてください。

That's all. Thank you for your hard work. It's a childish implementation, but if you customize it in various ways, it will be a decent online shop. The settlement system can use the external service such as [stripe]. Take advantage of Djnago's built-in libraries, and do a lot more.





See You Next Page!