Published Date : 2020年6月20日11:53

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


This blog has an English translation


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

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

Djangoを使って簡単なログイン機能を持ったオンラインショップのようなサイトを作ってみましょう。 Part8となる今回の動画シリーズでは、出品者が自身の商品の情報を編集したり、削除できるようにします。 その為に画像の登録機能と、削除する時にjavascriptとBootstrapを利用してモーダルを表示させて選択を促すような仕組みを実装していきます。

Use Django to create a site that looks like an online store with a simple login feature. In this video series, Part 8, Allows sellers to edit or delete their own product information. For this purpose, we will implement a function to register images and a mechanism to display modal to prompt selection when deleting by using javascript and Bootstrap.

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

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

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


目次

Table of Contents




① modelとurl
① model and url



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')

    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
    )
    paid_amount = models.FloatField()
    timestamp = models.DateField(default=timezone.now)

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

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

from django.core.validators import FileExtensionValidator
import uuid
import os

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

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):
.............................................................................
.............................................................................
.............................................................................
.............................................................................
    image = models.ImageField(verbose_name='商品画像',
                              validators=[
                                  FileExtensionValidator(['jpg', 'png'])],
                              upload_to=product_image_filename,
                              null=True,
                              default='product_images/000/000.png')

product_image_filename関数は引数にinstance, filenameがあります。 instanceはinstance.seller.idとあるように、class Itemから作られたインスタンスです。

分かりやすくすると、A商品=Item()となっている時、A商品がインスタンスです。

filenameは「file://xxx/xxx.jpg」の[xxx.jpg]の部分です。

この時、Itemのmodels.ImageFieldにあるupload_toはDjangoのclass FileFieldに定義してあります。

The product_image_filename function has an instance, filename argument. instance is an instance created from a class Item, as shown in instance.seller.id.

To make it easier to understand, when A product = Item (), A product is an instance.

filename is the [xxx.jpg] part of such as [file://xxx/xxx.jpg].

The upload_to in the models.ImageField of Item is defined in the class FileField of Django.

class FileField(Field):
.................................................................
.................................................................
    def generate_filename(self, instance, filename):
        if callable(self.upload_to):
            filename = self.upload_to(instance, filename)
        else:
            .......................................
            .......................................
        return self.storage.generate_filename(filename)
      

関数のように扱えるなら(callable)upload_toをそのまま実行します。

つまり、[def product_image_filename(instance, filename)]を実行します。

product_image_filenameにあるuuid.uuid4()はPythonに提供されているuuidモジュールのメソッドで、ランダムなバイト列を生成して返します。

そのランダムなバイト列を文字列に直してos.path.joinを使ってproduct_images/000/000.pngのような形にして、self.storage.generate_filename(filename)を使って画像を保存します。

If it works like a function (callable), just run upload_to.

That is, run [def product_image_filename(instance, filename)].

uuid.uuid4() in product_image_filename is a method of the uuid module provided in Python that generates and returns a random byte sequence.

We convert the random bytes into a string, using os.path.join to form it like product_images/000/000.png, and self.storage.generate_filename(filename) to store the image.

この時[settings.py]で画像を保存する場所を決めてあげると、自動で画像格納用のフォルダが作られます。

At this time, if you decide where to save the image by [settings.py], a folder for storing the image is automatically created.


settings.py

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

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

次は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'),

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

[edititem/<int:item_id>]等はFormタグにあるactionに用意されるURLです。HTMLは用意されてません。

例えばFormタグのButtonが押された時にPOSTリクエストがviewsのedit_item関数に送られて、商品の登録等の処理が行われます。

Such as [edititem/<int:item_id>] is the URL provided for the action in the Form tag. No HTML is provided.

When the Button is pressed of Form tag, a POST request is sent to the view's edit_item fuction for processing such as registering the item.

[edititem/<int:item_id>]の[<int:item_id>]はedititemview.htmlが開かれた時、既に送られているitem.idをint型の引数として渡すことを意味しています。

[<int:item_id>] in [edititem/<int:item_id>] means that when editemview.html is opened, it passes the already sent item.id as an int argument.


Responsive image


Responsive image




② view
② view



続いて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
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)
            else:
                is_seller = False
                items = Item.objects.all()
            context = {'message': f"ようこそ、{username}さん",
                       'items': items, 'isSeller': is_seller}
            return render(request, 'demoshop/home.html', context)
        return redirect('demoshop:index')
    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)
                cart_items = []
                for transaction in transactions:
                    cart_items.append(transaction.item)
                return render(request, 'demoshop/cartview.html', {
                    'message': "商品カゴ", 'cart_items': cart_items})
        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:
                    user = request.user

                    title = request.GET.get('title')
                    description = request.GET.get('description')
                    price = request.GET.get('price')
                    try:
                        uploaded_item = Item.objects.filter(
                            title=title, description=description,
                            price=price, seller=user.seller.id)
                        image_file = uploaded_item[0].image
                    except BaseException:
                        return render(request,
                                      'demoshop/registrationview.html',
                                      {'message': "商品を登録する",
                                       'isSeller': True})

                    context = {
                        'title': title,
                        'description': description,
                        'isSeller': True,
                        'price': price,
                        'check': check,
                        'imagefile': image_file,
                        'message': "商品は登録されました"}
                    return render(
                        request, 'demoshop/registrationview.html', context)
                else:
                    return render(request, 'demoshop/registrationview.html', {
                        'message': "商品を登録する", 'isSeller': True})
            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']

        seller = user.seller

        """
        ユーザーのタイプミスはHTML側で防ぐようになっていますが、
        Python側でも念のためタイプミスを防ぐようにします。
        """
        """
        User typos are prevented by HTML,
        but as a precaution, I also write Python to prevent typos.
        """
        if not title and not description and not price:
            # Please enter all the items to be registered of the product.
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '商品の登録項目は全て入力してください。'})
        try:
            float_price = float(price)
        except BaseException:
            # The price entered is not a number.
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '値段が数ではありません。'})
        if float_price <= 0:
            # Prices equal to or less than 0 cannot be registered.
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '0以下の値段は登録できません。'})
        if float_price >= 1000000:
            # Cannot register the price over 1 million yen.
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '100万円以上の値段は登録できません。'})
        if len(title) > 30:
            # Titles longer than 30 characters cannot be registered.
            length = f"{title}: {len(title)}文字"
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '30文字を超えるタイトルは登録できません。',
                           'length': length})
        if len(description) > 90:
            # Product descriptions longer than 90 characters cannot be
            # registered.
            length = f"{description}: {len(description)}文字"
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': '90文字を超える商品説明は登録できません。',
                           'length': length})

        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}"})

        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."})

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

            parameters = urlencode({'title': title, 'description': description,
                                    'price': price, 'check': registered})
            url = f'{redirect_url}?{parameters}'

            Item.objects.create(title=title, description=description,
                                price=price, image=imageFile, seller=seller)
        except BaseException:
            return render(request, 'demoshop/registrationview.html',
                          {'error_message': 'Something is wrong. Please go back and try again.'})

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


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']

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

            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."})

            item.title = title

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

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

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

            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}"})

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

            is_updated = 'updated'
            is_seller = True

            try:
                item.save()
            except BaseException:
                context = ({'message': "商品は更新されました",
                            '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})

            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
            context = {
                'message': "商品情報を更新します",
                'item': item,
                'isSeller': is_seller,
                'status': is_update}
            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}
            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')

前回から修正した部分、加えた部分が幾つかありますが、大事なのはedit_item関数の中のitem.save()部分です。

There are some modifications and additions from the last time, but what is important is the item.save() part of the edit_item function.

商品の情報を更新する際にupdateメソッドは使わずに、saveメソッドを使うのが上手くいくコツです。(自分はupdateメソッドによるエラーに苦しめられた)

The trick is to use the save method instead of the update method to update the product information. (I suffered from errors caused by the update method.)




③ templates
③ templates



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

Next is HTML in templates.


base.html

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

            {% if request.path == "/home" %}
            <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>

            {% elif 'registrationview' in request.get_full_path %}
            <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</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 'edititemview' in request.get_full_path %}
            <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:home' %}" class="text-white">Home</a></li>

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

            {% endif %}

            {% else %}

            {% if request.path == "/home" %}
            <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</a></li>
            <li><a href="{% url 'demoshop:cartview' %}" class="text-white">Cart</a></li>

            {% elif request.path == "/cartview" %}
            <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</a></li>
            <li><a href="{% url 'demoshop:home' %}" class="text-white">Home</a></li>

            {% elif request.path == "/signout" %}
            <li><a href="{% url 'demoshop:signin' %}" class="text-white">Sign in</a></li>
            <li><a href="{% url 'demoshop:index' %}" class="text-white">shop</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>
            {% endif %}

            {% 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 %}
</main>


</body>


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">
        <h2>{{ message }}</h2>
    </div>
    {% endif %}
</section>

<div class="album py-5 bg-light">
    <div class="container">

        <div class="row">

            {% for item in items %}
            <div class="col-md-4">
                <div class="card mb-4 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 %}
                    <img class="card-img-top" width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    {% endif %}
                    <div class="card-body">
                        <p class="card-text">{{ item.description }}</p>
                        <div class="text-center">
                            <button type="button"
                                class="title-btn btn btn-lg btn-outline-secondary">{{ item.title }}</button>
                            <button type="button"
                                class="price-btn btn btn-lg btn-outline-secondary">¥{{ item.price|floatformat|intcomma }}</button>
                        </div>
                    </div>
                </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-4">
                <div class="card mb-4 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 %}
                    <img class="card-img-top" width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    {% endif %}
                    <div class="card-body">
                        <p class="card-text">{{ item.description }}</p>
                        <div class="text-center">
                            <button type="button"
                                class="title-btn btn btn-lg btn-outline-secondary">{{ item.title }}</button>
                            <button type="button"
                                class="price-btn btn btn-lg btn-outline-secondary">¥{{ item.price|floatformat|intcomma }}</button>
                        </div>
                    </div>
                </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' %}
<div class="container">
    <div class="text-center">
        <div class="row bg-faded">
            <div class="col-4 mx-auto text-center">
                <div class="mx-auto d-block">
                    <div class="text-center">
                        <p>画像も削除されました</p>
                    </div>
                    <div class="text-center">
                        <p>{{ description }}</p>
                        <button type="button" class="title-btn btn btn-lg btn-outline-secondary">{{ title }}</button>
                        <button type="button"
                            class="price-btn btn btn-lg btn-outline-secondary">¥{{ price|floatformat|intcomma }}</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% 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 -->
        <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 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' %}
<div class="container">
    <div class="text-center">
        <div class="row bg-faded">
            <div class="col-4 mx-auto text-center">
                <img class="mx-auto d-block" width="100%" height="225" src="{{ imageFile.url }}" alt="Thumbnail">
                <div class="mx-auto d-block">
                    <div class="text-center">
                        <p>{{ description }}</p>
                        <button type="button" class="title-btn btn btn-lg btn-outline-secondary">{{ title }}</button>
                        <button type="button"
                            class="price-btn btn btn-lg btn-outline-secondary">¥{{ price|floatformat|intcomma }}</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% else %}
<div class="album py-5 bg-light">
    <div class="container">

        <div class="row">
            {% for item in items %}
            <div class="col-md-4">
                <div class="card mb-4 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 %}
                    <img class="card-img-top" width="100%" height="225" src="{{ item.image.url }}" alt="Thumbnail">
                    {% endif %}
                    <div class="card-body">
                        <p class="card-text">{{ item.description }}</p>
                        <div class="text-center">
                            <button type="button"
                                class="title-btn btn btn-lg btn-outline-secondary">{{ item.title }}</button>
                            <button type="button"
                                class="price-btn btn btn-lg btn-outline-secondary">¥{{ item.price|floatformat|intcomma }}</button>
                        </div>
                        <div class="text-center">
                            <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>
            </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 %}

削除をするかしないかをユーザーに促すモーダルを表示させます。そしてそのモーダル側からPOSTリクエストを送る為に、Javascriptを使用します。

Displays a modal that prompts the user whether or not to delete. It then uses Javascript to send POST requests from the modal side.


Responsive image



以上です。お疲れ様です。

That's all. Thank you for your hard work.





See You Next Page!