From c22e803864adf024eb0fc861ac4bb7d962d9458e Mon Sep 17 00:00:00 2001
From: Jude Hutton <jude2.hutton@live.uwe.ac.uk>
Date: Tue, 11 Apr 2023 08:33:07 +0100
Subject: [PATCH] Implemeted checkout page and order summary

---
 requirements.txt                   |   3 +-
 store/routes.py                    | 161 ++++++++++++++++++++++++++---
 store/static/_main.css             |  56 ++++++++++
 store/templates/basket.html        | 142 ++++++++++++-------------
 store/templates/checkout.html      | 114 ++++++++++++++++++++
 store/templates/item_sets.html     |   1 +
 store/templates/items.html         |   3 +-
 store/templates/order_summary.html |  72 +++++++++++++
 store/utility.py                   |   4 +
 9 files changed, 467 insertions(+), 89 deletions(-)
 create mode 100644 store/templates/checkout.html
 create mode 100644 store/templates/order_summary.html

diff --git a/requirements.txt b/requirements.txt
index 9de8bb8..701fd76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,5 @@ Flask_SQLAlchemy
 Flask_WTF
 WTForms
 flask_login
-flask_mail
\ No newline at end of file
+flask_mail
+requests
\ No newline at end of file
diff --git a/store/routes.py b/store/routes.py
index e63b9cc..0009d09 100644
--- a/store/routes.py
+++ b/store/routes.py
@@ -1,5 +1,19 @@
 from store import app, db
 from flask import render_template, request, flash, redirect, url_for, Flask, session
+from flask_wtf import FlaskForm
+from flask_wtf.csrf import generate_csrf
+import requests
+import datetime
+import re
+from wtforms import (
+    Form,
+    StringField,
+    validators,
+    SelectField,
+    TextAreaField,
+    ValidationError,
+    HiddenField,
+)
 import json
 from store.utility import *
 from store.forms import *
@@ -60,22 +74,6 @@ def index():
     return render_template("index.html", title="Home")
 
 
-@app.route("/basket")
-def basket():
-    items = []
-    total_price = 0
-    if "basket" not in session:
-        session["basket"] = {}
-    else:
-        for item_id, item in session["basket"].items():
-            items.append(item)
-            total_price = total_price + item["price"]
-    print(session["basket"])
-    return render_template(
-        "basket.html", title="Basket", items=items, total_price=total_price
-    )
-
-
 @app.route("/items")
 def items():
     unsold_items = get_unsold_items()
@@ -387,6 +385,135 @@ def ChangePhNumber():
     return render_template("userContent/ChangePhNumber.html")
 
 
+def validate_cvv_2(form, field):
+    regex = "^[0-9]{3,4}$"
+    p = re.compile(regex)
+    if not re.search(p, field.data):
+        raise ValidationError("CVV2 is incorrect")
+
+
+# This type of validation does not work for some edge cases such as PO-boxes, post offices etc
+# def validate_postcode(form, field):
+#     regex = "^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$"
+#     p = re.compile(regex)
+#     if not re.search(p, field.data):
+#         raise ValidationError("Postcode is not valid")
+
+
+def validate_address(form, field):
+    # Uses the google maps geocoding api to check if a postcode or address exists
+
+    url = "https://maps.googleapis.com/maps/api/geocode/json"
+
+    api_key = "AIzaSyCz4xyIl5yaIg869orL07ye2ainw7kv5Pc"
+
+    params = {"address": field, "key": api_key}
+
+    response = requests.get(url, params=params).json()
+
+    # Check if response status is "OK" and has at least one result
+    if response["status"] == "OK" and len(response["results"]) > 0:
+        print("Valid address or postcode")
+        pass
+    else:
+        raise ValidationError("Enter valid address")
+
+
+class CheckoutForm(Form):
+    months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+    today = datetime.date.today()
+    year = int(today.year)
+    years = [year]
+    for year in range(year, year + 50, 1):
+        years.append(year)
+    name = StringField(
+        "Name on card", [validators.Length(min=2, max=30), validators.DataRequired()]
+    )
+    card_number = StringField(
+        "Card Number", [validators.Length(min=15, max=16), validators.DataRequired()]
+    )
+    exp_date_month = SelectField("Month", [validators.DataRequired()], choices=months)
+    exp_date_year = SelectField("Year", [validators.DataRequired()], choices=years)
+    cvv_2 = StringField(
+        "CVV2",
+        [validators.Length(min=3, max=4), validators.DataRequired(), validate_cvv_2],
+    )
+    address = TextAreaField(
+        "Address",
+        [
+            validators.Length(min=10, max=250),
+            validators.DataRequired(),
+            validate_address,
+        ],
+    )
+    postcode = StringField(
+        "Postcode",
+        [
+            validators.Length(min=6, max=10),
+            validators.DataRequired(),
+            validate_address,
+        ],
+    )
+    # Cross-site request forgery token to make sure requests are sent from our site
+    csrf_token = HiddenField()
+
+
+@app.route("/order_summary")
+def order_summary():
+    total_price = 0
+    items = []
+    address = session.get("address", "")
+    for item_id, item in session["basket"].items():
+        items.append(item)
+        total_price = total_price + item["price"]
+    session.clear()
+    return render_template(
+        "order_summary.html", items=items, address=address, total_price=total_price
+    )
+
+
+@app.route("/checkout", methods=["POST", "GET"])
+def checkout():
+    items = []
+    total_price = 0
+    form = CheckoutForm(request.form)
+    # Get items stored in basket and add to list
+    for item_id, item in session["basket"].items():
+        items.append(item)
+        total_price = total_price + item["price"]
+    if request.method == "POST" and form.validate():
+        session["address"] = str(form.address.data + ", " + form.postcode.data)
+        try:
+            # sell_items()
+            return redirect(url_for("order_summary"))
+        except:
+            flash("One or more items in basket have already been sold!")
+    csrf_token = generate_csrf()
+    return render_template(
+        "checkout.html",
+        form=form,
+        total_price=total_price,
+        csrf_token=csrf_token,
+        items=items,
+    )
+
+
+@app.route("/basket")
+def basket():
+    items = []
+    total_price = 0
+    if "basket" not in session:
+        session["basket"] = {}
+    else:
+        for item_id, item in session["basket"].items():
+            items.append(item)
+            total_price = total_price + item["price"]
+
+    return render_template(
+        "basket.html", title="Basket", items=items, total_price=total_price
+    )
+
+
 @app.route("/add_to_basket", methods=["POST"])
 def add_to_basket():
     item_id = request.form["item_id"]
@@ -406,7 +533,7 @@ def add_to_basket():
     else:
         session["basket"][item_id] = item_dict
         flash("Item added to basket: " + item_obj.description)
-        print(session["basket"])
+
     return redirect(url_for("item_page", item_id=item_id))
 
 
diff --git a/store/static/_main.css b/store/static/_main.css
index c8a47b1..4376306 100644
--- a/store/static/_main.css
+++ b/store/static/_main.css
@@ -121,6 +121,61 @@ section {
     margin: 0 auto;
 }
 
+@media only screen and (min-width: 768px) {
+    .col-1 {
+        width: 8.33%;
+    }
+
+    .col-2 {
+        width: 16.66%;
+    }
+
+    .col-3 {
+        width: 25%;
+    }
+
+    .col-4 {
+        width: 33.33%;
+    }
+
+    .col-5 {
+        width: 41.66%;
+    }
+
+    .col-6 {
+        width: 50%;
+    }
+
+    .col-7 {
+        width: 58.33%;
+    }
+
+    .col-8 {
+        width: 66.66%;
+    }
+
+    .col-9 {
+        width: 75%;
+    }
+
+    .col-10 {
+        width: 83.33%;
+    }
+
+    .col-11 {
+        width: 91.66%;
+    }
+
+    .col-12 {
+        width: 100%;
+    }
+}
+
+.content {
+    /* to stop content being hidden by the fixed footer */
+    padding-bottom: 100px;
+}
+
 body {
     line-height: 1;
 }
@@ -163,6 +218,7 @@ footer {
 
 
 
+
 .navbar li a {
     color: white;
     display: block;
diff --git a/store/templates/basket.html b/store/templates/basket.html
index 0b13036..e5ece96 100644
--- a/store/templates/basket.html
+++ b/store/templates/basket.html
@@ -1,73 +1,75 @@
-
-
 {% extends "base.html" %}
 
 {% block content %}
-    <h1>Basket</h1>
-    <style>        
-        table {
-            border-collapse: collapse;
-            width: 100%;
-        }
-        
-        th, td {
-            text-align: left;
-            padding: 8px;
-            border-bottom: 1px solid #ddd;
-        }
-        
-        th {
-            background-color: #000000;
-            color: white;
-        }
-        
-        .remove-button {
-            background-color: #f44336;
-            color: white;
-            border: none;
-            padding: 8px 16px;
-            text-align: center;
-            text-decoration: none;
-            display: inline-block;
-            font-size: 16px;
-            margin: 4px 2px;
-            cursor: pointer;
-        }
-    </style>
-    
-    {% if items %}
-        <table>
-            <thead>
-                <tr>
-                    <th>Item description</th>
-                    <th>Item price</th>
-                    <th></th>
-                </tr>
-            </thead>
-            <tbody>
-                {% for item in items %}
-                    <tr>
-                        <td>{{ item['description'] }}</td>
-                        <td>£{{ item['price'] }}</td>
-                        <td>
-                            <form method="POST" action="/remove_item">
-                                <input type="hidden" name="item" value="{{ item['id'] }}">
-                                <input type="submit" value="Remove">
-                            </form>
-                        </td>
-                    </tr>
-                {% endfor %}
-            </tbody>
-            <tfoot>
-                <tr>
-                    <td>Total price:</td>
-                    <td>£{{ total_price }}</td>
-                    <td></td>
-                </tr>
-            </tfoot>
-        </table>
-    {% else %}
-        <p>Your basket is empty.</p>
-    {% endif %}
-    
-{% endblock %}
+<h1>Basket</h1>
+<style>
+    table {
+        border-collapse: collapse;
+        width: 100%;
+    }
+
+    th,
+    td {
+        text-align: left;
+        padding: 8px;
+        border-bottom: 1px solid #ddd;
+    }
+
+    th {
+        background-color: #000000;
+        color: white;
+    }
+
+    .remove-button {
+        background-color: #f44336;
+        color: white;
+        border: none;
+        padding: 8px 16px;
+        text-align: center;
+        text-decoration: none;
+        display: inline-block;
+        font-size: 16px;
+        margin: 4px 2px;
+        cursor: pointer;
+    }
+</style>
+
+{% if items %}
+<div>
+    <table>
+        <thead>
+            <tr>
+                <th>Item description</th>
+                <th>Item price</th>
+                <th></th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for item in items %}
+            <tr>
+                <td>{{ item['description'] }}</td>
+                <td>£{{ item['price'] }}</td>
+                <td>
+                    <form method="POST" action="/remove_item">
+                        <input type="hidden" name="item" value="{{ item['id'] }}">
+                        <input type="submit" value="Remove">
+                    </form>
+                </td>
+            </tr>
+            {% endfor %}
+        </tbody>
+        <tfoot>
+            <tr>
+                <td>Total price:</td>
+                <td>£{{ total_price }}</td>
+                <td></td>
+            </tr>
+        </tfoot>
+    </table>
+    <a href="{{url_for('checkout', total_price = total_price)}}">Checkout</a>
+</div>
+{% else %}
+<p>Your basket is empty.</p>
+{% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/store/templates/checkout.html b/store/templates/checkout.html
new file mode 100644
index 0000000..bbd426d
--- /dev/null
+++ b/store/templates/checkout.html
@@ -0,0 +1,114 @@
+{% extends 'base.html' %}
+
+{% block title %} Checkout {% endblock %}
+
+{% block content %}
+
+<style>
+    h1 {
+        padding: 15px;
+    }
+
+    .grid-container-element {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        grid-gap: 20px;
+        width: 90%;
+        padding: 10px;
+    }
+
+    .grid-child-element {
+        margin: 10px;
+        padding: 10px;
+    }
+
+    form {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+    }
+
+    label {
+        display: inline-block;
+        width: 150px;
+        text-align: left;
+        margin-right: 10px;
+        padding: 5px;
+    }
+
+    input[type="text"],
+    select {
+        margin-left: 10px;
+        outline: none;
+        padding: 5px;
+        border: 1px solid gray;
+    }
+
+    table {
+        border-collapse: collapse;
+        width: 100%;
+    }
+
+    th,
+    td {
+        text-align: left;
+        padding: 8px;
+        border-bottom: 1px solid #ddd;
+    }
+
+    th {
+        background-color: #000000;
+        color: white;
+    }
+</style>
+<div class="grid-container-element">
+    <div class="grid-child-element">
+        <h1>Checkout</h1>
+        <form method='POST' class="content">
+            {{ form.csrf_token }}
+            <br />
+            <label for="name">{{ form.name.label }}{{ form.name }}</label><br />
+            <br />
+            <label for="card_number">{{ form.card_number.label }}{{ form.card_number }}</label><br />
+            <br />
+            <label for="exp_date_month">{{ form.exp_date_month.label }}{{ form.exp_date_month }}</label>
+            <label for="exp_date_year">{{ form.exp_date_year.label }}{{ form.exp_date_year }}</label><br />
+            <br />
+            <label for="cvv_2">{{ form.cvv_2.label }}{{ form.cvv_2 }}</label><br />
+            <br />
+            <label for="address">{{form.address.label}}{{form.address}}</label><br />
+            <br />
+            <label for="postcode">{{form.postcode.label}}{{form.postcode}}</label><br />
+            <br />
+            <p><input type="submit" value="Complete payment"></p>
+        </form>
+    </div>
+    <div class="grid-child-element">
+        <h1>Order Summary</h1>
+        <table class="content">
+            <thead>
+                <tr>
+                    <th>Item description</th>
+                    <th>Item price</th>
+                    <th></th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for item in items %}
+                <tr>
+                    <td>{{ item['description'] }}</td>
+                    <td>£{{ item['price'] }}</td>
+                </tr>
+                {% endfor %}
+            </tbody>
+            <tfoot>
+                <tr>
+                    <td>Total price:</td>
+                    <td>£{{ total_price }}</td>
+                    <td></td>
+                </tr>
+            </tfoot>
+        </table>
+    </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/store/templates/item_sets.html b/store/templates/item_sets.html
index 27bae07..09e104d 100644
--- a/store/templates/item_sets.html
+++ b/store/templates/item_sets.html
@@ -10,6 +10,7 @@
         grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
         grid-gap: 10px;
         grid-auto-rows: minmax(100px, auto);
+        padding-bottom: 100px;
     }
 
     .grid-item {
diff --git a/store/templates/items.html b/store/templates/items.html
index 7f7c464..6704ee9 100644
--- a/store/templates/items.html
+++ b/store/templates/items.html
@@ -10,6 +10,7 @@
     grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
     grid-gap: 10px;
     grid-auto-rows: minmax(100px, auto);
+    padding-bottom: 100px;
   }
 
   .grid-item {
@@ -52,4 +53,4 @@
   <p>No items available</p>
   {% endif %}
 </div>
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/store/templates/order_summary.html b/store/templates/order_summary.html
new file mode 100644
index 0000000..579a91d
--- /dev/null
+++ b/store/templates/order_summary.html
@@ -0,0 +1,72 @@
+{%extends 'base.html' %}
+
+{% block title %} Order Summary {% endblock %}
+
+{% block content %}
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>Order Summary</title>
+    <style>
+        h1 {
+            margin-top: 0;
+        }
+
+        table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-top: 20px;
+        }
+
+        table th,
+        table td {
+            padding: 8px;
+            text-align: left;
+            border-bottom: 1px solid #ddd;
+        }
+
+        .total {
+            font-weight: bold;
+        }
+
+        .address {
+            margin-top: 20px;
+        }
+    </style>
+</head>
+
+<body class="content">
+    <h1>Order Summary</h1>
+
+    <table>
+        <thead>
+            <tr>
+                <th>Item</th>
+                <th>Price</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for item in items %}
+            <tr>
+                <td>{{ item["description"] }}</td>
+                <td>£{{ item["price"] }}</td>
+            </tr>
+            {% endfor %}
+            <tr class="total">
+                <td>Total:</td>
+                <td>£{{ total_price }}</td>
+            </tr>
+        </tbody>
+    </table>
+
+    <div class="address">
+        <h2>Delivery Address</h2>
+        <p>{{ address }}</p>
+    </div>
+</body>
+
+</html>
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/store/utility.py b/store/utility.py
index aae06c2..e1d9cac 100644
--- a/store/utility.py
+++ b/store/utility.py
@@ -1,6 +1,7 @@
 # TODO: Search for similiarly named item to replace removed item from itemset.
 
 from difflib import SequenceMatcher
+from wtforms import ValidationError
 from store.models import *
 from sqlalchemy import select, func
 import datetime
@@ -151,3 +152,6 @@ def update_set(reference_item):
 
 def get_item_by_id(id):
     return Item.query.get(id)
+
+
+
-- 
GitLab