diff --git a/requirements.txt b/requirements.txt index 9de8bb8dc83b5c38a249f8ad2c36b6c448f5c897..701fd76f05f9990acd121b3a2b725449cb866079 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 e63b9cc3fd83d4fe3871c7446eac7ef1bccb2779..0009d09fe34db43236165fefbbb77b538e3d9f3b 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 c8a47b1c5ef278d1bf21d526315bd954eae6ddcb..4376306f01243e36ee6fdb8daef6548a6c7a699d 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 0b1303660ad3bfe976f593c2c0de5251439da655..e5ece960fcb7db9fc589507db810482d2e46e009 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 0000000000000000000000000000000000000000..bbd426d5772a66927fa6d8616c83173b108967c1 --- /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 27bae077dd6b90b93e0b67dac8fd13e4ec478f16..09e104dfee390ce96836849139ede00890cf913c 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 7f7c46477394a56bd2b9967ae50fd9b506ac8d9d..6704ee9b0177ca1d60220acfa67ae23ea418b3a3 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 0000000000000000000000000000000000000000..579a91d99ba262f62680d09adc8c6776d7281fc3 --- /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 aae06c2a84caa914ac84e37bfaf957b068039aba..e1d9cac2108e65345268d20a9d083379c3911b89 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) + + +