↓↓クリックして頂けると励みになります。
【28 | 荷物受取先フォーム】 << 【ホーム】 >> 【30 | 配達先フォーム】
Google Cloud PlatformにGoogleアカウントでログインしてAPIキーを取得してください。
手順は以下の通りにお願いします。
mrradiology.hatenablog.jp
「core/templates/customer/create_job.html」ファイルに記述を追加します。
7行目の部分では、「key=」と「&callback」の間に作成したAPIキーを貼り付けてください。
記述追加 【Desktop/crowdsource/core/templates/customer/create_job.html】6行目
{% extends 'base.html' %} {% load bootstrap4 %} {% block head %} <script src="https://maps.googleapis.com/maps/api/js?key=ご自分のAPIキーを貼り付けてください&callback=initMap&libraries=places&v=weekly" defer></script> <style> #pills-tab a { color: black; } #pills-tab a:hover { color: green; text-decoration: none; } #pills-tab a.active { color: red; } #pickup-map { height: 100%; } </style> {% endblock %} {% block content %} <div class="container mt-4"> <div class="row"> <!-- 左側 --> <div class="col-lg-4"> <div class="card"> <div class="card-header"> 配達依頼の概要 </div> <div class="card-body"> {% if not job %} <p>配達依頼があればここに表示されます。</p> {% else %} <h4>{{ job.name }}</h4> <span>数量:{{ job.quantity }}</span><br /> <span>大きさ:{{ job.get_size_display }}</span> {% endif %} </div> </div> </div> <!-- 右側 --> <div class="col-lg-8"> <!-- Step tabs --> <div class="card mb-5"> <div class="card-body"> <ul class="nav nav-pills nav-justified align-items-center mb-3" id="pills-tab" role="tablist"> <li class="nav-item" role="presentation"> <a class="{% if step == 1 %}active{% endif %}" id="pills-info-tab" data-toggle="pill" href="#pills-info" role="tab" aria-controls="pills-info" aria-selected="true">配達依頼内容</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 2 %}active{% endif %}" id="pills-pickup-tab" data-toggle="pill" href="#pills-pickup" role="tab" aria-controls="pills-pickup" aria-selected="false">荷物受取先</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 3 %}active{% endif %}" id="pills-delivery-tab" data-toggle="pill" href="#pills-delivery" role="tab" aria-controls="pills-delivery" aria-selected="false">配達先</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 4 %}active{% endif %}" id="pills-payment-tab" data-toggle="pill" href="#pills-payment" role="tab" aria-controls="pills-payment" aria-selected="false">支払い</a> </li> </ul> </div> </div> <!-- Step forms --> <b>配達依頼作成</b> <div class="tab-content" id="pills-tabContent"> <!-- Step 1 --> <div class="tab-pane fade {% if step == 1 %}show active{% endif %}" id="pills-info" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>配達依頼内容</h1> <form method="POST" enctype="multipart/form-data"> <b class="text-secondary">情報</b><br /> <div class="card bg-white mt-2 mb-5"> <div class="card-body"> {% csrf_token %} {% bootstrap_form step1_form %} </div> </div> <input type="hidden" name="step" value="1"> <button type="submit" class="btn btn-info">保存して続ける</button> </form> </div> <!-- Step 2 --> <div class="tab-pane fade {% if step == 2 %}show active{% endif %}" id="pills-pickup" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>荷物受取先</h1> <form method="POST" enctype="multipart/form-data"> <b class="text-secondary">情報</b><br /> <div class="card bg-white mt-2 mb-5"> <div class="card-body"> <div class="row"> <div class="col-lg-8"> {% csrf_token %} {% bootstrap_form step2_form exclude='pickup_lat, pickup_lng' %} <input hidden id="pickup_lat" name="pickup_lat" value="{{ job.pickup_lat }}" /> <input hidden id="pickup_lng" name="pickup_lng" value="{{ job.pickup_lng }}"> </div> <div class="col-lg-4"> <div id="pickup-map"></div> <div id="pickup-infowindow-content"> <img src="" width="16" height="16" id="pickup-place-icon" /> <span id="pickup-place-name" class="title"></span><br /> <span id="pickup-place-address"></span> </div> </div> </div> </div> </div> <input type="hidden" name="step" value="2"> <button type="button" class="btn btn-outline-info" onclick="$('#pills-info-tab').tab('show');">戻る</button> <button type="submit" class="btn btn-info">保存して続ける</button> </form> </div> <!-- Step 3 --> <div class="tab-pane fade {% if step == 3 %}show active{% endif %}" id="pills-delivery" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>配達先</h1> </div> <!-- Step 4 --> <div class="tab-pane fade {% if step == 4 %}show active{% endif %}" id="pills-payment" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>支払い</h1> </div> </div> </div> </div> <script> function initMap() { const map = new google.maps.Map(document.getElementById("pickup-map"), { center: { lat: 43.062087, lng: 141.354404 }, zoom: 12, }); const input = document.getElementById("id_pickup_address"); const autocomplete = new google.maps.places.Autocomplete(input); // Bind the map's bounds (viewport) property to the autocomplete object, // so that the autocomplete requests use the current map bounds for the // bounds option in the request. autocomplete.bindTo("bounds", map); // Set the data fields to return when the user selects a place. autocomplete.setFields(["address_components", "geometry", "icon", "name"]); const infowindow = new google.maps.InfoWindow(); const infowindowContent = document.getElementById("pickup-infowindow-content"); infowindow.setContent(infowindowContent); const marker = new google.maps.Marker({ map, anchorPoint: new google.maps.Point(0, -29), }); autocomplete.addListener("place_changed", () => { infowindow.close(); marker.setVisible(false); const place = autocomplete.getPlace(); if (!place.geometry) { // User entered the name of a Place that was not suggested and // pressed the Enter key, or the Place Details request failed. window.alert("No details available for input: '" + place.name + "'"); return; } // If the place has a geometry, then present it on a map. if (place.geometry.viewport) { map.fitBounds(place.geometry.viewport); } else { map.setCenter(place.geometry.location); map.setZoom(17); // Why 17? Because it looks good. } marker.setPosition(place.geometry.location); marker.setVisible(true); let address = ""; if (place.address_components) { address = [ (place.address_components[0] && place.address_components[0].short_name) || "", (place.address_components[1] && place.address_components[1].short_name) || "", (place.address_components[2] && place.address_components[2].short_name) || "", ].join(" "); } infowindowContent.children["pickup-place-icon"].src = place.icon; infowindowContent.children["pickup-place-name"].textContent = place.name; infowindowContent.children["pickup-place-address"].textContent = address; infowindow.open(map, marker); $("#pickup_lat").val(place.geometry.location.lat()); $("#pickup_lng").val(place.geometry.location.lng()); }); } </script> {% endblock %}
ブラウザを確認します。
http://127.0.0.1:8000/customer/create_job/
住所とマップの位置が連動するようになりました。
「core/customer/views.py」ファイルを編集します。
記述編集 【Desktop/crowdsource/core/customer/views.py】138行目
import firebase_admin from firebase_admin import credentials, auth import stripe from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.urls import reverse from core.customer import forms from django.contrib import messages from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth import update_session_auth_hash from django.conf import settings from core.models import * cred = credentials.Certificate(settings.FIREBASE_ADMIN_CREDENTIAL) firebase_admin.initialize_app(cred) stripe.api_key = settings.STRIPE_API_SECRET_KEY @login_required() def home(request): return redirect(reverse('customer:profile')) @login_required(login_url="/sign-in/?next=/customer/") def profile_page(request): user_form = forms.BasicUserForm(instance=request.user) customer_form = forms.BasicCustomerForm(instance=request.user.customer) password_form = PasswordChangeForm(request.user) if request.method == "POST": if request.POST.get('action') == 'update_profile': user_form = forms.BasicUserForm(request.POST, instance=request.user) customer_form = forms.BasicCustomerForm(request.POST, request.FILES, instance=request.user.customer) if user_form.is_valid() and customer_form.is_valid(): user_form.save() customer_form.save() messages.success(request, 'プロフィールが更新されました。') return redirect(reverse('customer:profile')) elif request.POST.get('action') == 'update_password': password_form = PasswordChangeForm(request.user, request.POST) if password_form.is_valid(): user = password_form.save() update_session_auth_hash(request, user) messages.success(request, 'パスワードが更新されました。') return redirect(reverse('customer:profile')) elif request.POST.get('action') == 'update_phone': # Get Firebase user data firebase_user = auth.verify_id_token(request.POST.get('id_token')) request.user.customer.phone_number = firebase_user['phone_number'] request.user.customer.save() messages.success(request, '電話番号が更新されました。') return redirect(reverse('customer:profile')) return render(request, 'customer/profile.html', { "user_form": user_form, "customer_form": customer_form, "password_form": password_form, }) @login_required(login_url="/sign-in/?next=/customer/") def payment_method_page(request): current_customer = request.user.customer # Remove existing card if request.method == "POST": stripe.PaymentMethod.detach(current_customer.stripe_payment_method_id) current_customer.stripe_payment_method_id = "" current_customer.stripe_card_last4 = "" current_customer.save() return redirect(reverse('customer:payment_method')) # Save stripe customer infor if not current_customer.stripe_customer_id: customer = stripe.Customer.create() current_customer.stripe_customer_id = customer['id'] current_customer.save() # Get Stripe payment method stripe_payment_methods = stripe.PaymentMethod.list( customer = current_customer.stripe_customer_id, type = "card", ) print(stripe_payment_methods) if stripe_payment_methods and len(stripe_payment_methods.data) > 0: payment_method = stripe_payment_methods.data[0] current_customer.stripe_payment_method_id = payment_method.id current_customer.stripe_card_last4 = payment_method.card.last4 current_customer.save() else: current_customer.stripe_payment_method_id = "" current_customer.stripe_card_last4 = "" current_customer.save() if not current_customer.stripe_payment_method_id: intent = stripe.SetupIntent.create( customer = current_customer.stripe_customer_id ) return render(request, 'customer/payment_method.html', { "client_secret": intent.client_secret, "STRIPE_API_PUBLIC_KEY": settings.STRIPE_API_PUBLIC_KEY, }) else: return render(request, 'customer/payment_method.html') @login_required(login_url="/sign-in/?next=/customer/") def create_job_page(request): current_customer = request.user.customer if not current_customer.stripe_payment_method_id: return redirect(reverse('customer:payment_method')) creating_job = Job.objects.filter(customer=current_customer, status=Job.CREATING_STATUS).last() step1_form = forms.JobCreateStep1Form(instance=creating_job) step2_form = forms.JobCreateStep2Form(instance=creating_job) if request.method == "POST": if request.POST.get('step') == '1': step1_form = forms.JobCreateStep1Form(request.POST, request.FILES) if step1_form.is_valid(): creating_job = step1_form.save(commit=False) creating_job.customer = current_customer creating_job.save() return redirect(reverse('customer:create_job')) elif request.POST.get('step') == '2': step2_form = forms.JobCreateStep2Form(request.POST, instance=creating_job) if step2_form.is_valid(): creating_job = step2_form.save() return redirect(reverse('customer:create_job')) # 現在のステップ if not creating_job: current_step = 1 else: current_step = 2 return render(request, 'customer/create_job.html', { "job": creating_job, "step": current_step, "step1_form": step1_form, "step2_form": step2_form, })
配達依頼人の名前、住所、電話番号を登録できます。
管理サイトを確認します。
保存した後、次のステップに移動できるように実装します。
「core/templates/customer/create_job.html」ファイルを編集します。
記述追加 【Desktop/crowdsource/core/templates/customer/create_job.html】6行目
{% extends 'base.html' %} {% load bootstrap4 %} {% block head %} <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCApwNw0nn5uJ76G50pXQOqmHMY5sMUNIo&callback=initMap&libraries=places&v=weekly" defer></script> <style> #pills-tab a { color: black; } #pills-tab a:hover { color: green; text-decoration: none; } #pills-tab a.active { color: red; } #pickup-map { height: 100%; } </style> {% endblock %} {% block content %} <div class="container mt-4"> <div class="row"> <!-- 左側 --> <div class="col-lg-4"> <div class="card"> <div class="card-header"> 配達依頼の概要 </div> <div class="card-body"> {% if not job %} <p>配達依頼があればここに表示されます。</p> {% else %} {% if step > 1 %} <h4>{{ job.name }}</h4> <span>数量:{{ job.quantity }}</span><br /> <span>大きさ:{{ job.get_size_display }}</span> {% endif %} {% if step > 2 %} <hr /> <p class="text-secondary"><small><b>荷物受取先</b></small></p> 依頼人氏名:<h4>{{ job.pickup_name }}</h4> <span>住所:{{ job.pickup_address }}</span><br /> {% endif %} {% endif %} </div> </div> </div> <!-- 右側 --> <div class="col-lg-8"> <!-- Step tabs --> <div class="card mb-5"> <div class="card-body"> <ul class="nav nav-pills nav-justified align-items-center mb-3" id="pills-tab" role="tablist"> <li class="nav-item" role="presentation"> <a class="{% if step == 1 %}active{% endif %}" id="pills-info-tab" data-toggle="pill" href="#pills-info" role="tab" aria-controls="pills-info" aria-selected="true">配達依頼内容</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 2 %}active{% endif %}" id="pills-pickup-tab" data-toggle="pill" href="#pills-pickup" role="tab" aria-controls="pills-pickup" aria-selected="false">荷物受取先</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 3 %}active{% endif %}" id="pills-delivery-tab" data-toggle="pill" href="#pills-delivery" role="tab" aria-controls="pills-delivery" aria-selected="false">配達先</a> </li> <i class="fas fa-chevron-right"></i> <li class="nav-item" role="presentation"> <a class="{% if step == 4 %}active{% endif %}" id="pills-payment-tab" data-toggle="pill" href="#pills-payment" role="tab" aria-controls="pills-payment" aria-selected="false">支払い</a> </li> </ul> </div> </div> <!-- Step forms --> <b>配達依頼作成</b> <div class="tab-content" id="pills-tabContent"> <!-- Step 1 --> <div class="tab-pane fade {% if step == 1 %}show active{% endif %}" id="pills-info" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>配達依頼内容</h1> <form method="POST" enctype="multipart/form-data"> <b class="text-secondary">情報</b><br /> <div class="card bg-white mt-2 mb-5"> <div class="card-body"> {% csrf_token %} {% bootstrap_form step1_form %} </div> </div> <input type="hidden" name="step" value="1"> <button type="submit" class="btn btn-info">保存して続ける</button> </form> </div> <!-- Step 2 --> <div class="tab-pane fade {% if step == 2 %}show active{% endif %}" id="pills-pickup" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>荷物受取先</h1> <form method="POST" enctype="multipart/form-data"> <b class="text-secondary">情報</b><br /> <div class="card bg-white mt-2 mb-5"> <div class="card-body"> <div class="row"> <div class="col-lg-8"> {% csrf_token %} {% bootstrap_form step2_form exclude='pickup_lat, pickup_lng' %} <input hidden id="pickup_lat" name="pickup_lat" value="{{ job.pickup_lat }}" /> <input hidden id="pickup_lng" name="pickup_lng" value="{{ job.pickup_lng }}"> </div> <div class="col-lg-4"> <div id="pickup-map"></div> <div id="pickup-infowindow-content"> <img src="" width="16" height="16" id="pickup-place-icon" /> <span id="pickup-place-name" class="title"></span><br /> <span id="pickup-place-address"></span> </div> </div> </div> </div> </div> <input type="hidden" name="step" value="2"> <button type="button" class="btn btn-outline-info" onclick="$('#pills-info-tab').tab('show');">戻る</button> <button type="submit" class="btn btn-info">保存して続ける</button> </form> </div> <!-- Step 3 --> <div class="tab-pane fade {% if step == 3 %}show active{% endif %}" id="pills-delivery" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>配達先</h1> </div> <!-- Step 4 --> <div class="tab-pane fade {% if step == 4 %}show active{% endif %}" id="pills-payment" role="tabpanel" aria-labelledby="pills-info-tab"> <h1>支払い</h1> </div> </div> </div> </div> <script> var pickupLat = parseFloat('{{ job.pickup_lat }}'); var pickupLng = parseFloat('{{ job.pickup_lng }}'); function initMap() { const map = new google.maps.Map(document.getElementById("pickup-map"), { center: { lat: pickupLat || 43.062087, lng: pickupLng || 141.354404 }, zoom: 12, }); if (pickupLat && pickupLng) { new google.maps.Marker({ position: new google.maps.LatLng(pickupLat, pickupLng), map: map, }) } const input = document.getElementById("id_pickup_address"); const autocomplete = new google.maps.places.Autocomplete(input); // Bind the map's bounds (viewport) property to the autocomplete object, // so that the autocomplete requests use the current map bounds for the // bounds option in the request. autocomplete.bindTo("bounds", map); // Set the data fields to return when the user selects a place. autocomplete.setFields(["address_components", "geometry", "icon", "name"]); const infowindow = new google.maps.InfoWindow(); const infowindowContent = document.getElementById("pickup-infowindow-content"); infowindow.setContent(infowindowContent); const marker = new google.maps.Marker({ map, anchorPoint: new google.maps.Point(0, -29), }); autocomplete.addListener("place_changed", () => { infowindow.close(); marker.setVisible(false); const place = autocomplete.getPlace(); if (!place.geometry) { // User entered the name of a Place that was not suggested and // pressed the Enter key, or the Place Details request failed. window.alert("No details available for input: '" + place.name + "'"); return; } // If the place has a geometry, then present it on a map. if (place.geometry.viewport) { map.fitBounds(place.geometry.viewport); } else { map.setCenter(place.geometry.location); map.setZoom(17); // Why 17? Because it looks good. } marker.setPosition(place.geometry.location); marker.setVisible(true); let address = ""; if (place.address_components) { address = [ (place.address_components[0] && place.address_components[0].short_name) || "", (place.address_components[1] && place.address_components[1].short_name) || "", (place.address_components[2] && place.address_components[2].short_name) || "", ].join(" "); } infowindowContent.children["pickup-place-icon"].src = place.icon; infowindowContent.children["pickup-place-name"].textContent = place.name; infowindowContent.children["pickup-place-address"].textContent = address; infowindow.open(map, marker); $("#pickup_lat").val(place.geometry.location.lat()); $("#pickup_lng").val(place.geometry.location.lng()); }); } </script> {% endblock %}
「core/customer/views.py」ファイルを編集します。
記述編集 【Desktop/crowdsource/core/customer/views.py】
import firebase_admin from firebase_admin import credentials, auth import stripe from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.urls import reverse from core.customer import forms from django.contrib import messages from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth import update_session_auth_hash from django.conf import settings from core.models import * cred = credentials.Certificate(settings.FIREBASE_ADMIN_CREDENTIAL) firebase_admin.initialize_app(cred) stripe.api_key = settings.STRIPE_API_SECRET_KEY @login_required() def home(request): return redirect(reverse('customer:profile')) @login_required(login_url="/sign-in/?next=/customer/") def profile_page(request): user_form = forms.BasicUserForm(instance=request.user) customer_form = forms.BasicCustomerForm(instance=request.user.customer) password_form = PasswordChangeForm(request.user) if request.method == "POST": if request.POST.get('action') == 'update_profile': user_form = forms.BasicUserForm(request.POST, instance=request.user) customer_form = forms.BasicCustomerForm(request.POST, request.FILES, instance=request.user.customer) if user_form.is_valid() and customer_form.is_valid(): user_form.save() customer_form.save() messages.success(request, 'プロフィールが更新されました。') return redirect(reverse('customer:profile')) elif request.POST.get('action') == 'update_password': password_form = PasswordChangeForm(request.user, request.POST) if password_form.is_valid(): user = password_form.save() update_session_auth_hash(request, user) messages.success(request, 'パスワードが更新されました。') return redirect(reverse('customer:profile')) elif request.POST.get('action') == 'update_phone': # Get Firebase user data firebase_user = auth.verify_id_token(request.POST.get('id_token')) request.user.customer.phone_number = firebase_user['phone_number'] request.user.customer.save() messages.success(request, '電話番号が更新されました。') return redirect(reverse('customer:profile')) return render(request, 'customer/profile.html', { "user_form": user_form, "customer_form": customer_form, "password_form": password_form, }) @login_required(login_url="/sign-in/?next=/customer/") def payment_method_page(request): current_customer = request.user.customer # Remove existing card if request.method == "POST": stripe.PaymentMethod.detach(current_customer.stripe_payment_method_id) current_customer.stripe_payment_method_id = "" current_customer.stripe_card_last4 = "" current_customer.save() return redirect(reverse('customer:payment_method')) # Save stripe customer infor if not current_customer.stripe_customer_id: customer = stripe.Customer.create() current_customer.stripe_customer_id = customer['id'] current_customer.save() # Get Stripe payment method stripe_payment_methods = stripe.PaymentMethod.list( customer = current_customer.stripe_customer_id, type = "card", ) print(stripe_payment_methods) if stripe_payment_methods and len(stripe_payment_methods.data) > 0: payment_method = stripe_payment_methods.data[0] current_customer.stripe_payment_method_id = payment_method.id current_customer.stripe_card_last4 = payment_method.card.last4 current_customer.save() else: current_customer.stripe_payment_method_id = "" current_customer.stripe_card_last4 = "" current_customer.save() if not current_customer.stripe_payment_method_id: intent = stripe.SetupIntent.create( customer = current_customer.stripe_customer_id ) return render(request, 'customer/payment_method.html', { "client_secret": intent.client_secret, "STRIPE_API_PUBLIC_KEY": settings.STRIPE_API_PUBLIC_KEY, }) else: return render(request, 'customer/payment_method.html') @login_required(login_url="/sign-in/?next=/customer/") def create_job_page(request): current_customer = request.user.customer if not current_customer.stripe_payment_method_id: return redirect(reverse('customer:payment_method')) creating_job = Job.objects.filter(customer=current_customer, status=Job.CREATING_STATUS).last() step1_form = forms.JobCreateStep1Form(instance=creating_job) step2_form = forms.JobCreateStep2Form(instance=creating_job) if request.method == "POST": if request.POST.get('step') == '1': step1_form = forms.JobCreateStep1Form(request.POST, request.FILES) if step1_form.is_valid(): creating_job = step1_form.save(commit=False) creating_job.customer = current_customer creating_job.save() return redirect(reverse('customer:create_job')) elif request.POST.get('step') == '2': step2_form = forms.JobCreateStep2Form(request.POST, instance=creating_job) if step2_form.is_valid(): creating_job = step2_form.save() return redirect(reverse('customer:create_job')) # 現在のステップ if not creating_job: current_step = 1 elif creating_job.pickup_name: current_step = 3 else: current_step = 2 return render(request, 'customer/create_job.html', { "job": creating_job, "step": current_step, "step1_form": step1_form, "step2_form": step2_form, })
保存すると次のステップに進むようになりました。
↓↓クリックして頂けると励みになります。
【28 | 荷物受取先フォーム】 << 【ホーム】 >> 【30 | 配達先フォーム】