Published Date : 2020年12月31日18:57
ニコニコ動画にアップした動画のまとめ記事です。
This is a summary blog post about a video I uploaded to NicoNico.
細かい部分は動画を参考にしてください。
Please refer to the video for details.
目次
Table of Contents
では取引用のモデルの追加と、出品者ユーザーと出品物の紐づけを行っていきます。
Then, We will add a model for transaction and link the seller user and the item.
from django.db import models from django.contrib.auth.models import User from django.utils import timezone 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 = "出品者" 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) 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)
モデルが少し複雑になってきました。そんな時には画像でモデルの全体像を把握すると良いでしょう。
The model is getting a little more complex. In such a case, it is better to grasp the whole image of the model.
django_extensionsとgraphvizを使用すると簡単にモデルの全体像を可視化できます。
Using django_extensions and graphviz, you can easily visualize the entire model.
$ sudo apt-get install libgraphviz-dev graphviz pkg-config
$ pip install pygraphviz
$ pip install pydotplus
$ pip install django-extensions
INSTALLED_APPS = [ ..........................., 'django_extensions', ]
準備ができたらmanage.pyを使って図を作成できます。
When you are ready, you can use manage.py to create a model diagram.
python manage.py graph_models -a -g -o graph-model.png
-aはall-applications、 -gはgroup-models、-oはoutputで、後に「画像名」を付ける。この場合graph-modelとういう名前のpng画像ファイルを出力せよといった意味になる。
-a means all-applications, -g means group-models and -o means output, followed by "Image Name". In this case, means output a png image file named graph-model.
modelの変更をDBへ反映させる必要があるので、makemigrationsとmigrateを行う。
The model changes need to be reflected in the DB, so run makemigrations and migrate.
python manage.py makemigrations
python manage.py migrate
しかし、既にDBに行を登録してある場合は(例えばdemoshop_itemテーブルに商品として幾つか登録してある)オプションでデフォルト値を入力するか、models.pyにデフォルト値の設定をしてから再度makeigrationsを行う必要があります。
However, if you have already registered a row in the DB, you will need to enter a default value in the (For example, some items are registered in the demoshop_item table.) option or set a default value in models.py and then perform makemigrations again.
You are trying to add a non-nullable field 'seller' to item without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 1
今回はオプション1を選択して、デフォルト値として数字の1を入力してみました。今回の場合は上手くいきましたが、設定方法や既に登録してある行によって上手く機能しないことがありますので注意してください。
This time I chose option 1 and entered the number 1 as the default value. It worked fine in this case, but be aware that it may not work depending on how you configure it or what lines you have already registered.
dbshellを使えばどのようにDBへ登録されたかを確認できます。
You can use dbshell to see how it was registered in the DB.
~/djangodemo/demo $ python manage.py dbshell SQLite version 3.22.0 2018-01-22 18:45:57 Enter ".help" for usage hints. sqlite> sqlite> select * from demoshop_item; 1|1000.0|1|1|本1 2|3000.0|1|1|玩具1 3|2000.0|1|1|玩具2 4|1500.0|1|1|本2 5|500.0|1|1|マンガ sqlite> select * from demoshop_seller; 1|5 sqlite> .schema demoshop_item CREATE TABLE IF NOT EXISTS "demoshop_item" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "price" real NOT NULL, "description" text NOT NULL, "seller_id" integer NOT NULL REFERENCES "demoshop_seller" ("id") DEFERRABLE INITIALLY DEFERRED, "title" varchar(30) NOT NULL); sqlite> .quit ~/djangodemo/demo $
では次にurls.pyの設定をしていきます。
Now we'll set up urls.py.
from django.urls import path 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('registeritem', views.register_item, name='registeritem')
例えば上記のスクリプトなら、[http://xxxxxx/registeritem]にアクセスがあった場合にviews.pyにあるregister_itemを動かします。
register_itemは「商品を登録させる機能」を担っています。
そして、nameに「registeritem」と名前の指定をするとurlで「http://xxxxxx/registeritem」と書く代わりに[application name:registeritem]とテンプレート内に書くことができるようにしています。
For example, the above script runs the register_item function in views.py when [http://xxxxxx/registeritem] is accessed by the client.
If you specify [registeritem] as the name, you can write [application name:registeritem] in the template instead of writing [http://xxxxxx/registeritem] as the url.
個別のページや機能別の名前のURLにクライアントがアクセスした場合にviews.pyがそのページの表示や機能の処理を担当します。
When a client accesses an individual page or a URL with a function-specific name, views.py is responsible for displaying the page and handling the functionality.
from django.contrib.auth import authenticate, login, logout from django.shortcuts import render, redirect, reverse 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}) 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) inventories = [] for item in items: transactions = Transaction.objects.filter(item=item.id) for transaction in transactions: inventories.append(transaction) items = inventories 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 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') def cart_view(request): if request.method == "GET": user = request.user if request.user.is_authenticated: username = user.username 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, 'username': username}) else: return redirect('demoshop:index') else: return HttpResponse(status=500) def registration_view(request): if request.method == "GET": if request.user.is_authenticated: username = request.user.username 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') images_form = request.GET.get('imagesForm') context = {'title': title, 'description': description, 'isSeller': True, 'price': price, 'imagesForm': images_form, 'check': check, 'message': "商品は登録されました"} return render( request, 'demoshop/registrationview.html', context) else: return render(request, 'demoshop/registrationview.html', { 'message': "商品を登録する", 'isSeller': True, 'username': username}) else: return redirect('demoshop:home') else: return redirect('demoshop:index') else: return HttpResponse(status=500) def register_item(request): if request.method == "POST": redirect_url = reverse('demoshop:registrationview') title = request.POST['inputTitle'] description = request.POST['inputDescription'] price = request.POST['inputPrice'] """ ユーザーのタイプミスは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: # 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}) check = True parameters = urlencode({'title': title, 'description': description, 'price': price, 'check': check}) url = f'{redirect_url}?{parameters}' return redirect(url) else: return render(request, 'demoshop/registrationview.html', {'error_message': 'something wrong'}) def edititem_view(request): if request.method == "GET": if request.user.is_authenticated: username = request.user.username if request.user.groups.filter(name="sellers").count() != 0: return render(request, 'demoshop/edititemview.html', {'message': "商品を編集する", 'isSeller': True, 'username': username}) else: return redirect('demoshop:home') else: return redirect('demoshop:index') else: return HttpResponse(status=500)
Djangoで行っていることの基本はクライアント(Browser)が特定のURLにアクセスしたら(リクエスト)、それをViewを用いて内部で処理して(ModelがDBとやり取りをする)、Templateでそれをクライアント(Browser)に表示できるようにする(レスポンス)ことです。
The basic thing we do in Django is that once a client (Browser) has accessed a particular URL (Request), it can be processed internally using View (Model interacts with DB) and displayed to the client (Browser) using Template (Response).
最後にHTMLファイルの中身です。
Finally, the contents of the HTML file.
{% 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"> <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> <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>© デモショップ 2020</p> </div> </footer> {% endblock indexcontent %}
{% 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"> <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> <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>© デモショップ 2020</p> </div> </footer> {% endblock homecontent %}
<!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 request.path == "/signin" %} <li><a href="{% url 'demoshop:signupview' %}" class="text-white">Sign up</a></li> {% elif request.path == "/home" %} <li><a href="{% url 'demoshop:signout' %}" class="text-white">Sign out</a></li> {% if isSeller %} <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> {% else %} <li><a href="{% url 'demoshop:cartview' %}" class="text-white">Cart</a></li> {% endif %} {% 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> {% elif request.path == "/editview" or request.path == "/cartview" or request.path == "/registration" %} <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 'registrationview' in request.get_full_path or 'edititemview' in request.get_full_path and isSeller %} <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> {% else %} <li><a href="{% url 'demoshop:signupview' %}" class="text-white">Sign up</a></li> <li><a href="{% url 'demoshop:signin' %}" class="text-white">Sign in</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> <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> <script src="{% static 'demoshop/js/jquery-3.5.1.slim.min.js' %}"></script> <script src="{% static 'demoshop/js/bootstrap.bundle.js' %}"></script> </body> </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> </section> <div class="album py-5 bg-light"> <div class="container"> <div class="row"> {% for item in cart_items %} <div class="col-md-4"> <div class="card mb-4 shadow-sm"> <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> <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>© デモショップ 2020</p> </div> </footer> {% endblock cartviewcontent %}
{% 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> </div> </section> <footer class="text-muted"> <div class="container"> <p class="float-right"> <a href="#">Back to top</a> </p> <p>© デモショップ 2020</p> </div> </footer> {% endblock edititemviewcontent %}
{% 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;">{{ length }}</p> </div> </section> {% if check %} <div class="album py-5 bg-light"> <div class="container"> <div class="row"> <div class="col-md-4"> <div class="card mb-4 shadow-sm"> <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">{{ filename }}</text> </svg> <div class="card-body"> <p class="card-text">{{ description }}</p> <div class="text-center"> <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> </div> {% 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> <div class="form-group" style="margin: 1rem 3rem;"> <button class="btn btn-primary btn-block" type="submit" value="Edit">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>© デモショップ 2020</p> </div> </footer> {% endblock registrationviewcontent %}
{% extends "demoshop/base.html" %} {% load static %} <!-- Custom styles for this template --> {% block signincss %} <link href="{% static 'demoshop/css/signin.css' %}" rel="stylesheet">{% endblock signincss %} {% block signupviewcontent %} <div class="text-center"> {% if error_message %} <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal" style="color: red;">{{ error_message }}</h1> {% else %} <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal">Sign-up Example</h1> {% endif %} <form class="form-signin" action="{% url 'demoshop:signup' %}" method="post"> {% csrf_token %} <label for="inputUsername" class="sr-only">Username</label> <input type="text" name="inputUsername" id="inputUsername" class="form-control" placeholder="Username" required autofocus> <label for="inputEmail" class="sr-only">Email address</label> <input type="email" name="inputEmail" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> <label for="inputPassword" class="sr-only">Password</label> <input type="password" name="inputPassword" id="inputPassword" class="form-control" placeholder="Password" required> <div class="checkbox mb-3"> <label> <input type="checkbox" id="seller_checkbox" name="seller" value="seller"><span style="color: red; margin-left : 10px;">Sign up as a seller</span> </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit" value="Signup">Sign up</button> <p class="mt-5 mb-3 text-muted">© デモショップ 2020</p> </form> </div> {% endblock signupviewcontent %}
{% extends "demoshop/base.html" %} {% load static %} <!-- Custom styles for this template --> {% block signincss %} <link href="{% static 'demoshop/css/signin.css' %}" rel="stylesheet">{% endblock signincss %} {% block signincontent %} <div class="text-center"> {% if error_message %} <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal" style="color: red;">{{ error_message }}</h1> {% else %} <h1 class="custum-signin-h1 h3 mb-3 font-weight-normal">Signin Example</h1> {% endif %} <form class="form-signin" action="{% url 'demoshop:signinuser' %}" method="post"> {% csrf_token %} <label for="inputUsername" class="sr-only">Username</label> <input type="text" name="inputUsername" id="inputUsername" class="form-control" placeholder="Username" required autofocus> <label for="inputPassword" class="sr-only">Password</label> <input type="password" name="inputPassword" id="inputPassword" class="form-control" placeholder="Password" required> <button class="btn btn-lg btn-primary btn-block" type="submit" value="Signin">Sign in</button> <p class="mt-5 mb-3 text-muted">© デモショップ 2020</p> </form> </div> {% endblock signincontent %}
models.pyとurls.pyを実装していきませう。
Let's implement models.py and urls.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.
............................................. ............................................. MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
次はurls.pyです。
Next is 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.
続いてviews.pyです。
Next is 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.)
続いてテンプレートのHTMLです。
Next is HTML in templates.
<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>
{% 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>© デモショップ 2020</p> </div> </footer> {% endblock homecontent %}
{% 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>© デモショップ 2020</p> </div> </footer> {% endblock indexcontent %}
{% 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">×</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>© デモショップ 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.
models.pyとurls.pyを実装していきませう。
Let's implement models.py and urls.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.
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.
続いてviews.pyです。
Next is 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.
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.)
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.
補足ですが、[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()
テンプレート内で使用するテンプレートフィルタを作成したので、使い方を説明しておきます。
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 - ......... - .........
from django import template register = template.Library() @register.filter def indices(members, i): return members[int(i)]
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.
{% 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 %}
<select class="form-control" name="inputSelectNum" required> {% load get_range %} {% for n in item.stock|get_range %} <option>{{ n }}</option> {% endfor %} </select>
続いてテンプレートのHTMLです。
Next is HTML in templates.
<!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>
{% 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>© デモショップ 2020</p> </div> </footer> {% endblock homecontent %}
{% 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>© デモショップ 2020</p> </div> </footer> {% endblock indexcontent%}
{% 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">×</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>© デモショップ 2020</p> </div> </footer> {% endblock edititemviewcontent %}
{% 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>© デモショップ 2020</p> </div> </footer> {% endblock registrationviewcontent %}
{% 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">×</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>© デモショップ 2020</p> </div> </footer> {% endblock cartviewcontent %}
新しく作成したHTMLファイル
Newly created HTML files
{% 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>© デモショップ 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.
以上です。お疲れ様です。
That's all. Thank you for your hard work.