Store app.py deprecated.\nAdmin pages added: privacy notice, accessibility, data retention, license. Improvements to accessibility of those pages pending.

This commit is contained in:
2024-05-01 20:45:20 +01:00
parent a95ae3654b
commit 46f2f730e2
46 changed files with 3302 additions and 850 deletions

726
DEPRECATED_app.py Normal file
View File

@@ -0,0 +1,726 @@
"""
Project: PARTS Website
Author: Edward Middleton-Smith
Precision And Research Technology Systems Limited
Technology: App General
Feature: App
Description:
Initializes the Flask application, sets the configuration based on the environment, and defines two routes (/ and /about) that render templates with the specified titles.
"""
# IMPORTS
# VARIABLE INSTANTIATION
# METHODS
# IMPORTS
# internal
from config import app_config, Config
# from routes import bp_home
from app.forms import Form_Contact, Form_Basket_Add, Form_Basket_Edit, Form_Billing, Form_Delivery_Region, Form_Currency # , Form_Product
from models.model_view_base import Model_View_Base
from models.model_view_home import Model_View_Home
from models.model_view_store import Model_View_Store
from models.model_view_store_home import Model_View_Store_Home
from models.model_view_store_product import Model_View_Store_Product
from models.model_view_store_basket import Model_View_Store_Basket
from models.model_view_store_checkout import Model_View_Store_Checkout
from models.model_view_store_checkout_success import Model_View_Store_Checkout_Success
from models.model_view_contact import Model_View_Contact
from business_objects.product import Product # , Product_Image_Filters, Resolution_Level_Enum
import lib.argument_validation as av
from business_objects.basket import Basket_Item
from datastores.datastore_store import DataStore_Store
from business_objects.product import Product_Filters
# external
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail, Message
import stripe
import json
from dotenv import load_dotenv, find_dotenv
import os
import sys
from urllib.parse import quote_plus, urlencode
from authlib.integrations.flask_client import OAuth
import jwt
# VARIABLE INSTANTIATION
app = Flask(__name__)
CORS(app)
# app.register_blueprint(bp_home, url_prefix='')
app.config.from_object(app_config)
print(f'secret key = {app.secret_key}')
app.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI
app.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS
app.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT
app.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET
app.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0
app.ID_TOKEN_USER = Config.ID_TOKEN_USER
"""
app.is_included_VAT = Config.is_included_VAT
app.KEY_IS_INCLUDED_VAT = Config.KEY_IS_INCLUDED_VAT
app.code_currency = Config.code_currency
app.KEY_CODE_CURRENCY = Config.KEY_CODE_CURRENCY
app.code_region_delivery = Config.code_region_delivery
app.KEY_CODE_REGION_DELIVERY = Config.KEY_CODE_REGION_DELIVERY
app.KEY_ID_CURRENCY = Config.KEY_ID_CURRENCY
app.KEY_ID_REGION_DELIVERY = Config.KEY_ID_REGION_DELIVERY
app.id_currency = Config.id_currency
app.id_region_delivery = Config.id_region_delivery
"""
try:
# db = SQLAlchemy(app)
db = SQLAlchemy()
db.init_app(app)
with app.app_context():
db.create_all()
db.engine.url = app.config.SQLALCHEMY_DATABASE_URI
app.errors = 'none'
except Exception as e:
app.errors = str(e)
oauth = OAuth(app)
oauth.register(
"auth0",
client_id = app.ID_AUTH0_CLIENT,
client_secret = app.ID_AUTH0_CLIENT_SECRET, # =env.get("AUTH0_CLIENT_SECRET"),
client_kwargs={
"scope": "openid profile email",
},
server_metadata_url=f'https://{app.DOMAIN_AUTH0}/.well-known/openid-configuration'
)
# session[app.ID_TOKEN_USER] = {'userinfo': {'sub': ''}}
mail = Mail(app)
# METHODS
sys.path.insert(0, os.path.dirname(__file__))
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
message = 'It works!\n'
version = 'Python %s\n' % sys.version.split()[0]
response = '\n'.join([message, version])
return [response.encode()]
# ROUTING
@app.route('/', methods=['GET'])
def home():
return render_template('_page_home.html', model=Model_View_Home(db, get_info_user(), app))
@app.route('/contact', methods=['GET'])
def contact():
form = Form_Contact()
if form.validate_on_submit():
# Handle form submission
email = form.email.data
CC = form.CC.data # not in use
name = form.name.data
msg = form.msg.data
# send email
mailItem = Message("PARTS Website Contact Us Message", recipients=[app.config['MAIL_DEFAULT_SENDER']])
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{msg}\n\nKind regards,\n{name}\n{email}"
mail.send(mailItem)
return render_template('_page_contact.html', model=Model_View_Contact(db, get_info_user(), app, form))
@app.route('/services', methods=['GET', 'POST'])
@app.route('/public_html/services', methods=['GET', 'POST'])
def services():
return render_template('_page_services.html', model=Model_View_Home(db, get_info_user(), app))
# Store
@app.route('/store', methods=['GET', 'POST'])
def store_home():
print("store home")
try:
data = request.json
except:
data = {}
print(f'data={data}')
"""
try:
id_currency = data.id_currency
except:
id_currency = Model_View_Store.ID_CURRENCY_DEFAULT
try:
id_region_delivery = data.id_region_delivery
except:
id_region_delivery = Model_View_Store.ID_REGION_DELIVERY_DEFAULT
"""
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
print(f"id_currency = {id_currency}")
print(f"id_region_delivery = {id_region_delivery}")
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
# model.get_regions_and_currencies()
# model.categories = Model_View_Store_Home.get_many_product_category(db)
# product = categories[list(categories.keys())[0]][0]
model.get_many_product_category(Product_Filters(
model.id_user,
True, '', False,
True, '', False, False,
True, '', False,
False, '', False, True,
False, id_region_delivery, False,
False, id_currency, False,
True, '', False
))
"""
for cat in model.categories:
# for product in model.categories[cat]:
i_cat = model.category_index[cat.id_category]
for p in cat.products:
print(f'adding basket add form for product with id: {p.id_product}')
model.categories[i_cat].products[model.categories[i_cat].product_index[p.id_product]].add_form_basket_add()
model.categories[i_cat].products[model.categories[i_cat].product_index[p.id_product]].form_basket_add = Form_Basket_Add()
print('form added')
# print('form added for product {p.id}')
print('rendering page store home')
"""
for cat in model.category_list.categories:
# for product in model.categories[cat]:
# i_cat = model.category_index[cat.id_category]
print(f'category: {cat.name}')
for p in cat.products:
print(f'product: {p.name}')
print(f'selected permutation: {p.get_permutation_selected()}')
if request.method == 'GET':
return render_template('_page_store_home.html', model = model) # "<html><body><h1>Boobs</h1></html></body>"
else: # POST request
html_block = render_template('_block_store_home_body.html', model = model)
print(f'html_block:\n{html_block}')
return jsonify(Success=True, data={'html_block': html_block})
# update with local basket, if not logged in- partial
@app.route('/store/basket_load', methods=['POST'])
def basket_load():
_m = 'basket_load'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
# model, html_block = render_basket_from_JSON(data)
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data) # , Model_View_Store.KEY_BASKET)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
# model.import_JSON_basket(data)
model.get_basket(data)
# model.is_included_VAT = is_included_VAT
html_block = render_template('_block_store_basket.html', model = model)
print(f'html_block:\n{html_block}')
return jsonify(Success=True, data={'html_block': html_block, 'basket': model.basket.to_json()}) # {'items': [b_i.to_json() for b_i in model.basket]}}) # { 'html': render_template('_block_store_basket.html', model = model), 'basket': model.basket })
@app.route('/store/basket_add', methods=['POST'])
def basket_add():
_m = 'basket_add'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
# model.is_included_VAT = is_included_VAT
form_data = data[Model_View_Store.key_form]
"""
try:
form_data[Model_View_Store.KEY_VALUE_DEFAULT]
except KeyError:
form_data[Model_View_Store.KEY_VALUE_DEFAULT] =
"""
print(f'form_data: {form_data}')
form = Form_Basket_Add(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
try:
print('importing basket')
# print('success' if model.import_JSON_basket(data) else 'failure')
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data, form)
print(f'permutation_id: {permutation_id}\nquantity: {quantity}')
print(f'editing basket:')
model.basket_item_edit(permutation_id, quantity, True) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}}) # 'items': [b_i.to_json() for b_i in model.basket]}})
return jsonify({'status': 'failure', 'Message': 'Invalid quantities'})
@app.route('/store/basket_edit', methods=['POST'])
def basket_edit():
_m = 'basket_edit'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
form = Form_Basket_Edit(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
try:
# print('importing basket')
# model.import_JSON_basket(data)
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data, form)
model.basket_item_edit(permutation_id, quantity, False) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}})
return jsonify({'status': 'failure', 'Message': 'Invalid quantities'})
@app.route('/store/basket_delete', methods=['POST'])
def basket_delete():
_m = 'basket_delete'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
try:
# print('importing basket')
# model.import_JSON_basket(data)
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data)
model.basket_item_edit(permutation_id, 0, False) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}})
# delete basket item with product_id
item_deleted = False
for basket_item in model.basket:
if basket_item.product.id_product == product_id:
model.basket.remove(basket_item)
item_deleted = True
break
if not item_deleted:
return jsonify({'status': 'failure', 'Message': 'Basket item removal failure: product not found in basket.'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': [b_i.to_json() for b_i in model.basket]}})
@app.route('/store/basket', methods=['GET']) # 'POST'
def store_basket():
# _m = 'basket_review'
try:
data = request.json
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Basket(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
except:
raise Exception('Bad data received by controller')
return render_template('_page_store_basket.html', model=model)
data = request.json # .get('data')
print(f'data: {data}')
# if logged in
#html_block = render_template('_page_store_basket.html', model = model) # with basket from database
try:
print('importing basket')
model.import_JSON_basket(data)
except:
return jsonify({'status': 'failure', 'Message': 'Bad basket data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_page_store_billing.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': [b_i.to_json() for b_i in model.basket]}})
@app.route('/store/basket_info', methods=['POST'])
def basket_info():
_m = 'basket_info'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Basket(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
form = Form_Billing(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
data_info = {}
try:
info_type = data[model.key_info_type]
print('importing address information')
data_info[model.key_region] = form.region.data
print(f'region: {data_info[model.key_region]}')
data_info[model.key_name_full] = form.name_full.data
print(f'full name: {data_info[model.key_name_full]}')
data_info[model.key_phone_number] = form.phone_number.data
print(f'phone number: {data_info[model.key_phone_number]}')
data_info[model.key_postcode] = form.postcode.data
print(f'postcode: {data_info[model.key_postcode]}')
data_info[model.key_address1] = form.address_1.data
print(f'address line 1: {data_info[model.key_address1]}')
data_info[model.key_address2] = form.address_2.data
print(f'address line 2: {data_info[model.key_address2]}')
data_info[model.key_city] = form.city.data
print(f'city: {data_info[model.key_city]}')
data_info[model.key_county] = form.county.data
print(f'county: {data_info[model.key_county]}')
data_info[model.key_info_identical] = form.identical
print(f'identical: {data_info[model.key_info_identical]}')
except:
return jsonify({'status': 'failure', 'Message': 'Bad form data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
# html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
data = {}
data[model.key_info_type] = model.key_info_billing if (info_type == model.key_info_billing) else model.key_info_delivery
data[info_type] = data_info
return jsonify(Success = True, data = data)
return jsonify({'status': 'failure', 'Message': f'Invalid address information\n{form.errors}'})
@app.route('/store/product?permutationId=<permutation_id>regionId=&<region_id>&currencyId=<currency_id>&isIncludedVAT=<is_included_VAT>', methods=['GET']) # <product_id>&
def store_product(permutation_id, region_id, currency_id, is_included_VAT):
_m = 'store_product'
"""
av.full_val_int(product_id, 'product_id', _m)
product_id = int(product_id)
print(f'product_id: {product_id}')
if permutation_id == 'None' or str(type(permutation_id)) == "<class 'NoneType'>":
permutation_id = None
else:
"""
# app.id_region_delivery = region_id
# app.id_currency = currency_id
av.full_val_int(permutation_id, 'permutation_id', _m)
permutation_id = int(permutation_id)
print(f'{_m}\nstarting...')
# print(f'product id: {product_id}')
print(f'permutation id: {permutation_id}')
try:
model = Model_View_Store_Product(db, get_info_user(), app, permutation_id, currency_id, region_id, is_included_VAT)
print('model reached')
# model.id_currency, model.id_region_delivery, model.is_included_VAT = DataStore_Store.get_metadata_basket(request.json)
# model.get_many_product_category(product_ids = str(product_id))
# print('categories reached')
# product = model.categories[0].products[0]# [list(categories.keys())[0]][0]
# print('product reached')
# return jsonify({'data': render_template('_page_store_product.html', model=model)})
permutation_selected = model.product.get_permutation_selected()
print(f'selected permutation: {permutation_selected}')
return render_template('_page_store_product.html', model=model)
except:
print('except reached')
return jsonify({'status': 'error'})
# Stripe
@app.route('/config', methods=['GET'])
def get_publishable_key():
price = stripe.Price.retrieve(Model_View_Store_Checkout.get_price_id(Product.template().id))
return jsonify({
'publicKey': Model_View_Store_Checkout.key_public_stripe, # os.getenv('KEY_PUBLIC_STRIPE'),
'unitAmount': price['unit_amount'],
'currency': price['currency']
})
# Create Stripe prices
@app.route('/store/product_create_all', methods=['GET'])
def product_create_all():
# _m = 'product_create_all'
model = Model_View_Store_Checkout(db, get_info_user(), app)
products, currencies = model.get_many_product_new()
html = f"<html><body>"
for product in products:
product.id_stripe_product = model.create_product(product)
html += f"<h1>product id = {product.id}</h1><br><h1>id_stripe_product = {product.id_stripe_product}</h1>"
html += "</body></html>"
return html
@app.route('/store/price_create_all', methods=['GET'])
def price_create_all():
# _m = 'price_create_all'
model = Model_View_Store_Checkout(db, get_info_user(), app)
products, currencies = model.get_many_price_new()
html = f"<html><body>"
for product in products:
product.id_stripe_price = model.create_price(product, currencies[product.id])
html += f"<h1>product id = {product.id}</h1><br><h1>id_stripe_price = {product.id_stripe_price}</h1><br><h1>currency = {currencies[product.id]}</h1>"
html += "</body></html>"
return html
# Fetch the Checkout Session to display the JSON result on the success page
@app.route('/store/checkout_session?<session_id>', methods=['GET'])
def get_checkout_session(session_id):
id = request.args.get('session_id')
_m = 'get_checkout_session'
# av.full_val_int(session_id, 'session_id', _m)
av.val_str(session_id, 'session_id', _m)
print(f'url var normal session id: {session_id}')
print(f'{_m}\nstarting...')
# session_id = id
session_id = session_id
print(f'request.args checkout session id: {session_id}') # for function
checkout_session = stripe.checkout.Session.retrieve(session_id)
return jsonify(checkout_session)
@app.route('/store/checkout', methods=['POST', 'GET'])
def create_checkout_session():
# quantity = request.form.get('quantity', 1)
# domain_url = os.getenv('DOMAIN')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Checkout(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
print('checkout model created')
try:
data = request.json # .get('data')
print(f'data: {data}')
print('importing basket')
model.get_basket(data)
model.id_currency, model.id_region_delivery, model.is_included_VAT = DataStore_Store.get_metadata_basket(data)
# model.import_JSON_basket(data)
# print('getting is subscription')
# is_subscription_checkout_session = data[model.key_is_subscription]
code_currency = 'GBP' # data[model.key_code_currency]
print(f'currency code: {code_currency}')
except:
return jsonify({'status': 'failure', 'Message': 'Bad form data received by controller'})
items = []
for item in model.basket.items:
permutation = item.product.get_permutation_selected()
price = permutation.get_price_from_code_currency(code_currency)
items.append({'price': price.id_stripe_price,
'quantity': item.quantity })
# if is_subscription_checkout_session:
# break
print(f'items = {items}')
try:
# Create new Checkout Session for the order
# Other optional params include:
# [billing_address_collection] - to display billing address details on the page
# [customer] - if you have an existing Stripe Customer ID
# [payment_intent_data] - lets capture the payment later
# [customer_email] - lets you prefill the email input in the form
# [automatic_tax] - to automatically calculate sales tax, VAT and GST in the checkout page
# For full details see https://stripe.com/docs/api/checkout/sessions/create
stripe.api_key = model.key_secret_stripe
# ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
checkout_session = stripe.checkout.Session.create(
success_url=Model_View_Store_Checkout.url_host + '/store/checkout_success%3Fsession_id={CHECKOUT_SESSION_ID}',
cancel_url=Model_View_Store_Checkout.url_host + '/store/checkout_cancelled',
mode='subscription' if False else 'payment', # is_subscription_checkout_session
# automatic_tax={'enabled': True},
line_items=items
)
data_out = {}
# data_out['Success'] = True
data_out[f'{model.key_id_checkout}'] = checkout_session.id
data_out[f'{model.key_url_checkout}'] = checkout_session.url
# return jsonify(Success = True, data = data_out) # Success = True, f'{model.key_id_checkout}' = checkout_session.id)
print(f'checkout session url: {checkout_session.url}')
# redirect(checkout_session.url) # , code=303)
return jsonify(Success = True, data = data_out)
# return get_checkout_session(checkout_session.id)
except Exception as e:
return jsonify(error=str(e)), 403
@app.route('/store/checkout_success?<session_id>', methods=['GET'])
def checkout_success(session_id):
_m = 'store checkout success'
# av.full_val_int(session_id, 'session_id', _m)
av.val_str(session_id, 'session_id', _m)
if (session_id[:len('session_id=')] == 'session_id='):
session_id = session_id[len('session_id='):]
print(f'url var normal session id: {session_id}')
print(f'{_m}\nstarting...')
id = request.args.get('sessionId')
print(f'request.args checkout session id: {id}')
checkout_session = stripe.checkout.Session.retrieve(session_id)
print(f'checkout session data: {checkout_session}')
# validate billing information
model = Model_View_Store_Checkout_Success(db, get_info_user(), app)
return render_template('_page_store_checkout_success.html', model=model)
@app.route('/store/checkout_cancelled', methods=['GET'])
def checkout_cancelled():
_m = 'store checkout success'
print(f'{_m}\nstarting...')
return render_template('_page_store_checkout_cancelled.html', model=Model_View_Store_Checkout(db, get_info_user(), app))
# include VAT in prices?
@app.route('/store/set_is_included_VAT', methods=['POST'])
def set_is_included_VAT():
_m = 'set_is_included_VAT'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
app.is_included_VAT = not app.is_included_VAT # session[app.KEY_IS_INCLUDED_VAT] # data[app.KEY_IS_INCLUDED_VAT]
return jsonify(Success=True, data={Model_View_Base.KEY_IS_INCLUDED_VAT: app.is_included_VAT})
# delivery region
@app.route('/store/set_delivery_region', methods=['POST'])
def set_delivery_region():
_m = 'set_delivery_region'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
# model = Model_View_Store(db, get_info_user(), app)
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
"""
form = Form_Delivery_Region(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
app.id_region_delivery = form.id_region_delivery.data
"""
id_region_delivery = form_data[Model_View_Store.KEY_BASKET][Model_View_Base.KEY_ID_REGION_DELIVERY]
print(f'id_region_delivery: {id_region_delivery}')
return jsonify(Success=True, data={Model_View_Base.KEY_ID_REGION_DELIVERY: id_region_delivery})
# currency
@app.route('/store/set_currency', methods=['POST'])
def set_currency():
_m = 'set_currency'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
# model = Model_View_Store(db, get_info_user(), app)
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
"""
form = Form_Currency(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
app.id_currency = form.id_currency.data
print(f'id_currency: {app.id_currency}')
"""
app.id_currency = form_data[Model_View_Base.KEY_ID_CURRENCY]
print(f'id_currency: {app.id_currency}')
return jsonify(Success=True, data={Model_View_Base.KEY_ID_CURRENCY: app.id_currency})
# User authentication
@app.route("/login")
def login():
print(f'redirect uri: {url_for("login_callback", _external=True)}')
return oauth.auth0.authorize_redirect(
redirect_uri=url_for("login_callback", _external=True)
)
@app.route("/login_callback", methods=["GET", "POST"])
def login_callback():
token = oauth.auth0.authorize_access_token()
session[app.ID_TOKEN_USER] = token
# import user id
print(f'str(type(token)) = {str(type(token))}')
print(f'token = {token}')
userinfo = token.get('userinfo')
print(f'user info: {userinfo}')
# id_user = token.get('sub')
id_user = userinfo.get('sub')
print(f'user ID: {id_user}')
# id_user = get_id_user()
# add user to database
# DataStore_Store(db, userinfo).add_new_user(id_user) # this is part of get basket - should occur on page load
return redirect("/")
@app.route("/logout")
def logout():
session.clear()
return redirect(
"https://" + app.DOMAIN_AUTH0
+ "/v2/logout?"
+ urlencode(
{
"returnTo": url_for("home", _external=True),
"client_id": app.ID_AUTH0_CLIENT,
},
quote_via=quote_plus,
)
)
def get_info_user():
try:
return session[app.ID_TOKEN_USER].get('userinfo') # .get('sub')
except:
return {'sub': ''}
"""
@app.route('/send-email', methods=['GET'])
def send_email():
try:
msg = Message("Flask Mail test", recipients=[app.config['MAIL_DEFAULT_SENDER']])
msg.body = "Dear Lord Edward Middleton-Smith,\n\nThis is a test email sent from Flask.\n\nKind regards,\nBot"
mail.send(msg)
except:
return "<html>Error</html>"
return "<html>Email sent</html>"
"""
"""
@app.route('/test-mysql', methods=['GET'])
def test_mysql():
model = Model_View_Store(db, get_info_user(), app)
_m = 'test_mysql'
proc_string = f'CALL {proc_name}('
if has_arguments:
arg_keys = list(argument_dict_list.keys())
for i in range(len(arg_keys)):
proc_string += f'{"" if i == 0 else ", "}:{arg_keys[i]}'
proc_string += ')'
proc_string = text(proc_string)
print(f'{_m}\nproc_string: {proc_string}\nargs: {argument_dict_list}')
# with self.db.session.begin() as session:
if has_arguments:
result = self.db.session.execute(proc_string, argument_dict_list)
else:
result = self.db.session.execute(proc_string)
print(f'result: {result}')
return "<html>MySQL test</html>"
"""
@app.route('/public_html/403.shtml', methods=['GET'])
def error_403():
return "<html>Error 403</html>"
# Onload
if True or __name__ == '__main__':
app.run()
# app.run(debug=True, host="0.0.0.0", port=5000)

Binary file not shown.

Binary file not shown.

Binary file not shown.

787
app.py
View File

@@ -18,26 +18,18 @@ Initializes the Flask application, sets the configuration based on the environme
# internal # internal
from config import app_config, Config from config import app_config, Config
# from routes import bp_home # from routes import bp_home
from app.forms import Form_Contact, Form_Basket_Add, Form_Basket_Edit, Form_Billing, Form_Delivery_Region, Form_Currency # , Form_Product from forms import Form_Contact
from models.model_view_base import Model_View_Base from models.model_view_base import Model_View_Base
from models.model_view_home import Model_View_Home from models.model_view_home import Model_View_Home
from models.model_view_store import Model_View_Store
from models.model_view_store_home import Model_View_Store_Home
from models.model_view_store_product import Model_View_Store_Product
from models.model_view_store_basket import Model_View_Store_Basket
from models.model_view_store_checkout import Model_View_Store_Checkout
from models.model_view_store_checkout_success import Model_View_Store_Checkout_Success
from models.model_view_contact import Model_View_Contact from models.model_view_contact import Model_View_Contact
from business_objects.product import Product # , Product_Image_Filters, Resolution_Level_Enum from business_objects.product import Product # , Product_Image_Filters, Resolution_Level_Enum
import lib.argument_validation as av import lib.argument_validation as av
from business_objects.basket import Basket_Item
from datastores.datastore_store import DataStore_Store
from business_objects.product import Product_Filters
# external # external
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
from flask_cors import CORS from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail, Message from flask_mail import Mail, Message
from flask_wtf.csrf import CSRFProtect
import stripe import stripe
import json import json
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
@@ -50,42 +42,75 @@ import jwt
# VARIABLE INSTANTIATION # VARIABLE INSTANTIATION
app = Flask(__name__) app = Flask(__name__)
CORS(app)
# app.register_blueprint(bp_home, url_prefix='')
email_recipient = 'edward.middletonsmith@gmail.com' # 'noreply'
app.config.from_object(app_config) csrf = CSRFProtect(app)
print(f'secret key = {app.secret_key}') CORS(app)
# app.register_blueprint(bp_home, url_prefix=''
# app.config.from_object(app_config)
# app.app_config = app_config
app.DEBUG = Config.DEBUG
app.TESTING = Config.TESTING
app.SECRET_KEY = "007cfbdaaf6d1eb27209720e8a5fc8ba0a334ae0be6fcac132b0a471549cde7c" # Config.SECRET_KEY
app.config['SECRET_KEY'] = app.SECRET_KEY
app.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI app.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI
app.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS app.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS
app.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT app.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT
app.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET app.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET
app.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0 app.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0
app.ID_TOKEN_USER = Config.ID_TOKEN_USER app.ID_TOKEN_USER = Config.ID_TOKEN_USER
"""
app.is_included_VAT = Config.is_included_VAT
app.KEY_IS_INCLUDED_VAT = Config.KEY_IS_INCLUDED_VAT
app.code_currency = Config.code_currency
app.KEY_CODE_CURRENCY = Config.KEY_CODE_CURRENCY
app.code_region_delivery = Config.code_region_delivery
app.KEY_CODE_REGION_DELIVERY = Config.KEY_CODE_REGION_DELIVERY
app.KEY_ID_CURRENCY = Config.KEY_ID_CURRENCY
app.KEY_ID_REGION_DELIVERY = Config.KEY_ID_REGION_DELIVERY
app.id_currency = Config.id_currency
app.id_region_delivery = Config.id_region_delivery
"""
try: app.config.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI
# db = SQLAlchemy(app) app.config.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS
db = SQLAlchemy() app.config.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT
db.init_app(app) app.config.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET
with app.app_context(): app.config.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0
db.create_all() app.config.ID_TOKEN_USER = Config.ID_TOKEN_USER
db.engine.url = app.config.SQLALCHEMY_DATABASE_URI
app.errors = 'none'
except Exception as e:
app.errors = str(e)
app.config['SQLALCHEMY_DATABASE_URI'] = Config.SQLALCHEMY_DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = Config.SQLALCHEMY_TRACK_MODIFICATIONS
app.config['ID_AUTH0_CLIENT'] = Config.ID_AUTH0_CLIENT
app.config['ID_AUTH0_CLIENT_SECRET'] = Config.ID_AUTH0_CLIENT_SECRET
app.config['DOMAIN_AUTH0'] = Config.DOMAIN_AUTH0
app.config['ID_TOKEN_USER'] = Config.ID_TOKEN_USER
app.MAIL_SERVER = Config.MAIL_SERVER
app.MAIL_PORT = Config.MAIL_PORT
app.MAIL_USE_TLS = Config.MAIL_USE_TLS
app.MAIL_USERNAME = Config.MAIL_USERNAME
app.MAIL_PASSWORD = Config.MAIL_PASSWORD
app.MAIL_DEFAULT_SENDER = Config.MAIL_DEFAULT_SENDER
app.config.MAIL_SERVER = Config.MAIL_SERVER
app.config.MAIL_PORT = Config.MAIL_PORT
app.config.MAIL_USE_TLS = Config.MAIL_USE_TLS
app.config.MAIL_USERNAME = Config.MAIL_USERNAME
app.config.MAIL_PASSWORD = Config.MAIL_PASSWORD
app.config.MAIL_DEFAULT_SENDER = Config.MAIL_DEFAULT_SENDER
app.config['MAIL_SERVER'] = Config.MAIL_SERVER
app.config['MAIL_PORT'] = Config.MAIL_PORT
app.config['MAIL_USE_TLS'] = Config.MAIL_USE_TLS
app.config['MAIL_USERNAME'] = Config.MAIL_USERNAME
app.config['MAIL_PASSWORD'] = Config.MAIL_PASSWORD
app.config['MAIL_DEFAULT_SENDER'] = Config.MAIL_DEFAULT_SENDER
app.config['MAIL_CONTACT_PUBLIC'] = Config.MAIL_CONTACT_PUBLIC
# app.RECAPTCHA_PUBLIC_KEY = Config.RECAPTCHA_PUBLIC_KEY
app.config['RECAPTCHA_PUBLIC_KEY'] = Config.RECAPTCHA_PUBLIC_KEY
# app.RECAPTCHA_PRIVATE_KEY = Config.RECAPTCHA_PRIVATE_KEY
app.config['RECAPTCHA_PRIVATE_KEY'] = Config.RECAPTCHA_PRIVATE_KEY
# db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = Config.SQLALCHEMY_DATABASE_URI
db = SQLAlchemy()
db.init_app(app)
with app.app_context():
db.create_all()
db.engine.url = app.config.SQLALCHEMY_DATABASE_URI
"""
oauth = OAuth(app) oauth = OAuth(app)
oauth.register( oauth.register(
"auth0", "auth0",
@@ -97,13 +122,27 @@ oauth.register(
server_metadata_url=f'https://{app.DOMAIN_AUTH0}/.well-known/openid-configuration' server_metadata_url=f'https://{app.DOMAIN_AUTH0}/.well-known/openid-configuration'
) )
# session[app.ID_TOKEN_USER] = {'userinfo': {'sub': ''}} # session[app.ID_TOKEN_USER] = {'userinfo': {'sub': ''}}
"""
mail = Mail(app) mail = Mail(app)
# METHODS # METHODS
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
@app.route('/hello')
def hello():
return "Hello, World!" + f"{app.errors}\n{app.config.SQLALCHEMY_DATABASE_URI == app.SQLALCHEMY_DATABASE_URI}"
@app.route('/goodbye')
def goodbye():
return "Goodbye, cruel World!"
"""
@app.route('/public_html/403.shtml', methods=['GET'])
def forbidden():
return send_from_directory('/home/partsltd/public_html', '403.shtml')
"""
def application(environ, start_response): def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')]) start_response('200 OK', [('Content-Type', 'text/plain')])
@@ -116,563 +155,94 @@ def application(environ, start_response):
# ROUTING # ROUTING
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def home(): def home():
return render_template('_page_home.html', model=Model_View_Home(db, get_info_user(), app)) try:
model = Model_View_Home(db, get_info_user(), app)
html_body = render_template('_page_home.html', model = model)
except Exception as e:
return str(e)
return html_body
@app.route('/contact', methods=['GET']) @app.route('/contact', methods=['GET'])
def contact(): def contact():
form = Form_Contact()
if form.validate_on_submit():
# Handle form submission
email = form.email.data
CC = form.CC.data # not in use
name = form.name.data
msg = form.msg.data
# send email
mailItem = Message("PARTS Website Contact Us Message", recipients=[app.config['MAIL_DEFAULT_SENDER']])
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{msg}\n\nKind regards,\n{name}\n{email}"
mail.send(mailItem)
return render_template('_page_contact.html', model=Model_View_Contact(db, get_info_user(), app, form))
@app.route('/services', methods=['GET', 'POST'])
@app.route('/public_html/services', methods=['GET', 'POST'])
def services():
return render_template('_page_services.html', model=Model_View_Home(db, get_info_user(), app))
# Store
@app.route('/store', methods=['GET', 'POST'])
def store_home():
print("store home")
try: try:
data = request.json form = Form_Contact()
except: model = Model_View_Contact(db, get_info_user(), app, form)
data = {} html_body = render_template('_page_contact.html', model = model)
print(f'data={data}') except Exception as e:
""" return jsonify(error=str(e)), 403
return html_body
@app.route('/contact', methods=['POST'])
def contact_post():
try: try:
id_currency = data.id_currency form = Form_Contact()
except: if form.validate_on_submit():
id_currency = Model_View_Store.ID_CURRENCY_DEFAULT # Handle form submission
try: email = form.email.data
id_region_delivery = data.id_region_delivery CC = form.CC.data # not in use
except: name = form.name.data
id_region_delivery = Model_View_Store.ID_REGION_DELIVERY_DEFAULT message = form.message.data
""" # send email
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data) mailItem = Message("PARTS Website Contact Us Message", recipients=[app.config['MAIL_DEFAULT_SENDER']])
print(f"id_currency = {id_currency}") mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{message}\n\nKind regards,\n{name}\n{email}"
print(f"id_region_delivery = {id_region_delivery}") mail.send(mailItem)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT) return "Submitted."
# model.get_regions_and_currencies() return "Invalid. Failed to submit."
# model.categories = Model_View_Store_Home.get_many_product_category(db) # html_body = render_template('_page_contact.html', model = model)
# product = categories[list(categories.keys())[0]][0]
model.get_many_product_category(Product_Filters(
model.id_user,
True, '', False,
True, '', False, False,
True, '', False,
False, '', False, True,
False, id_region_delivery, False,
False, id_currency, False,
True, '', False
))
"""
for cat in model.categories:
# for product in model.categories[cat]:
i_cat = model.category_index[cat.id_category]
for p in cat.products:
print(f'adding basket add form for product with id: {p.id_product}')
model.categories[i_cat].products[model.categories[i_cat].product_index[p.id_product]].add_form_basket_add()
model.categories[i_cat].products[model.categories[i_cat].product_index[p.id_product]].form_basket_add = Form_Basket_Add()
print('form added')
# print('form added for product {p.id}')
print('rendering page store home')
"""
for cat in model.category_list.categories:
# for product in model.categories[cat]:
# i_cat = model.category_index[cat.id_category]
print(f'category: {cat.name}')
for p in cat.products:
print(f'product: {p.name}')
print(f'selected permutation: {p.get_permutation_selected()}')
if request.method == 'GET':
return render_template('_page_store_home.html', model = model) # "<html><body><h1>Boobs</h1></html></body>"
else: # POST request
html_block = render_template('_block_store_home_body.html', model = model)
print(f'html_block:\n{html_block}')
return jsonify(Success=True, data={'html_block': html_block})
# update with local basket, if not logged in- partial
@app.route('/store/basket_load', methods=['POST'])
def basket_load():
_m = 'basket_load'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
# model, html_block = render_basket_from_JSON(data)
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data) # , Model_View_Store.KEY_BASKET)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
# model.import_JSON_basket(data)
model.get_basket(data)
# model.is_included_VAT = is_included_VAT
html_block = render_template('_block_store_basket.html', model = model)
print(f'html_block:\n{html_block}')
return jsonify(Success=True, data={'html_block': html_block, 'basket': model.basket.to_json()}) # {'items': [b_i.to_json() for b_i in model.basket]}}) # { 'html': render_template('_block_store_basket.html', model = model), 'basket': model.basket })
@app.route('/store/basket_add', methods=['POST'])
def basket_add():
_m = 'basket_add'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
# model.is_included_VAT = is_included_VAT
form_data = data[Model_View_Store.key_form]
"""
try:
form_data[Model_View_Store.KEY_VALUE_DEFAULT]
except KeyError:
form_data[Model_View_Store.KEY_VALUE_DEFAULT] =
"""
print(f'form_data: {form_data}')
form = Form_Basket_Add(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
try:
print('importing basket')
# print('success' if model.import_JSON_basket(data) else 'failure')
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data, form)
print(f'permutation_id: {permutation_id}\nquantity: {quantity}')
print(f'editing basket:')
model.basket_item_edit(permutation_id, quantity, True) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}}) # 'items': [b_i.to_json() for b_i in model.basket]}})
return jsonify({'status': 'failure', 'Message': 'Invalid quantities'})
@app.route('/store/basket_edit', methods=['POST'])
def basket_edit():
_m = 'basket_edit'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
form = Form_Basket_Edit(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
try:
# print('importing basket')
# model.import_JSON_basket(data)
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data, form)
model.basket_item_edit(permutation_id, quantity, False) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}})
return jsonify({'status': 'failure', 'Message': 'Invalid quantities'})
@app.route('/store/basket_delete', methods=['POST'])
def basket_delete():
_m = 'basket_delete'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Home(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
try:
# print('importing basket')
# model.import_JSON_basket(data)
model.get_basket(data)
permutation_id, quantity = model.import_JSON_basket_item(data)
model.basket_item_edit(permutation_id, 0, False) # new_basket =
except:
return jsonify({'status': 'failure', 'Message': 'Bad data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': model.basket.to_json_list()}})
# delete basket item with product_id
item_deleted = False
for basket_item in model.basket:
if basket_item.product.id_product == product_id:
model.basket.remove(basket_item)
item_deleted = True
break
if not item_deleted:
return jsonify({'status': 'failure', 'Message': 'Basket item removal failure: product not found in basket.'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': [b_i.to_json() for b_i in model.basket]}})
@app.route('/store/basket', methods=['GET']) # 'POST'
def store_basket():
# _m = 'basket_review'
try:
data = request.json
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Basket(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
model.is_included_VAT = is_included_VAT
except:
raise Exception('Bad data received by controller')
return render_template('_page_store_basket.html', model=model)
data = request.json # .get('data')
print(f'data: {data}')
# if logged in
#html_block = render_template('_page_store_basket.html', model = model) # with basket from database
try:
print('importing basket')
model.import_JSON_basket(data)
except:
return jsonify({'status': 'failure', 'Message': 'Bad basket data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
html_block = render_template('_page_store_billing.html', model = model)
# print(f'html_block:\n{html_block}')
return jsonify(Success = True, data = { 'html_block': html_block, 'basket': {'items': [b_i.to_json() for b_i in model.basket]}})
@app.route('/store/basket_info', methods=['POST'])
def basket_info():
_m = 'basket_info'
data = request.json # .get('data')
print(f'data: {data}')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Basket(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
form = Form_Billing(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
print('valid form')
# model = input_JSON_basket(model, data)
# if not logged in:
data_info = {}
try:
info_type = data[model.key_info_type]
print('importing address information')
data_info[model.key_region] = form.region.data
print(f'region: {data_info[model.key_region]}')
data_info[model.key_name_full] = form.name_full.data
print(f'full name: {data_info[model.key_name_full]}')
data_info[model.key_phone_number] = form.phone_number.data
print(f'phone number: {data_info[model.key_phone_number]}')
data_info[model.key_postcode] = form.postcode.data
print(f'postcode: {data_info[model.key_postcode]}')
data_info[model.key_address1] = form.address_1.data
print(f'address line 1: {data_info[model.key_address1]}')
data_info[model.key_address2] = form.address_2.data
print(f'address line 2: {data_info[model.key_address2]}')
data_info[model.key_city] = form.city.data
print(f'city: {data_info[model.key_city]}')
data_info[model.key_county] = form.county.data
print(f'county: {data_info[model.key_county]}')
data_info[model.key_info_identical] = form.identical
print(f'identical: {data_info[model.key_info_identical]}')
except:
return jsonify({'status': 'failure', 'Message': 'Bad form data received by controller'})
# return jsonify(Success = True, data = { html_block: render_template(), Model_View_Store.key_basket: new_basket })
# html_block = render_template('_block_store_basket.html', model = model)
# print(f'html_block:\n{html_block}')
data = {}
data[model.key_info_type] = model.key_info_billing if (info_type == model.key_info_billing) else model.key_info_delivery
data[info_type] = data_info
return jsonify(Success = True, data = data)
return jsonify({'status': 'failure', 'Message': f'Invalid address information\n{form.errors}'})
@app.route('/store/product?permutationId=<permutation_id>regionId=&<region_id>&currencyId=<currency_id>&isIncludedVAT=<is_included_VAT>', methods=['GET']) # <product_id>&
def store_product(permutation_id, region_id, currency_id, is_included_VAT):
_m = 'store_product'
"""
av.full_val_int(product_id, 'product_id', _m)
product_id = int(product_id)
print(f'product_id: {product_id}')
if permutation_id == 'None' or str(type(permutation_id)) == "<class 'NoneType'>":
permutation_id = None
else:
"""
# app.id_region_delivery = region_id
# app.id_currency = currency_id
av.full_val_int(permutation_id, 'permutation_id', _m)
permutation_id = int(permutation_id)
print(f'{_m}\nstarting...')
# print(f'product id: {product_id}')
print(f'permutation id: {permutation_id}')
try:
model = Model_View_Store_Product(db, get_info_user(), app, permutation_id, currency_id, region_id, is_included_VAT)
print('model reached')
# model.id_currency, model.id_region_delivery, model.is_included_VAT = DataStore_Store.get_metadata_basket(request.json)
# model.get_many_product_category(product_ids = str(product_id))
# print('categories reached')
# product = model.categories[0].products[0]# [list(categories.keys())[0]][0]
# print('product reached')
# return jsonify({'data': render_template('_page_store_product.html', model=model)})
permutation_selected = model.product.get_permutation_selected()
print(f'selected permutation: {permutation_selected}')
return render_template('_page_store_product.html', model=model)
except:
print('except reached')
return jsonify({'status': 'error'})
# Stripe
@app.route('/config', methods=['GET'])
def get_publishable_key():
price = stripe.Price.retrieve(Model_View_Store_Checkout.get_price_id(Product.template().id))
return jsonify({
'publicKey': Model_View_Store_Checkout.key_public_stripe, # os.getenv('KEY_PUBLIC_STRIPE'),
'unitAmount': price['unit_amount'],
'currency': price['currency']
})
# Create Stripe prices
@app.route('/store/product_create_all', methods=['GET'])
def product_create_all():
# _m = 'product_create_all'
model = Model_View_Store_Checkout(db, get_info_user(), app)
products, currencies = model.get_many_product_new()
html = f"<html><body>"
for product in products:
product.id_stripe_product = model.create_product(product)
html += f"<h1>product id = {product.id}</h1><br><h1>id_stripe_product = {product.id_stripe_product}</h1>"
html += "</body></html>"
return html
@app.route('/store/price_create_all', methods=['GET'])
def price_create_all():
# _m = 'price_create_all'
model = Model_View_Store_Checkout(db, get_info_user(), app)
products, currencies = model.get_many_price_new()
html = f"<html><body>"
for product in products:
product.id_stripe_price = model.create_price(product, currencies[product.id])
html += f"<h1>product id = {product.id}</h1><br><h1>id_stripe_price = {product.id_stripe_price}</h1><br><h1>currency = {currencies[product.id]}</h1>"
html += "</body></html>"
return html
# Fetch the Checkout Session to display the JSON result on the success page
@app.route('/store/checkout_session?<session_id>', methods=['GET'])
def get_checkout_session(session_id):
id = request.args.get('session_id')
_m = 'get_checkout_session'
# av.full_val_int(session_id, 'session_id', _m)
av.val_str(session_id, 'session_id', _m)
print(f'url var normal session id: {session_id}')
print(f'{_m}\nstarting...')
# session_id = id
session_id = session_id
print(f'request.args checkout session id: {session_id}') # for function
checkout_session = stripe.checkout.Session.retrieve(session_id)
return jsonify(checkout_session)
@app.route('/store/checkout', methods=['POST', 'GET'])
def create_checkout_session():
# quantity = request.form.get('quantity', 1)
# domain_url = os.getenv('DOMAIN')
id_currency, id_region_delivery, is_included_VAT = DataStore_Store.get_metadata_basket(data)
model = Model_View_Store_Checkout(db, get_info_user(), app, id_currency, id_region_delivery, is_included_VAT)
print('checkout model created')
try:
data = request.json # .get('data')
print(f'data: {data}')
print('importing basket')
model.get_basket(data)
model.id_currency, model.id_region_delivery, model.is_included_VAT = DataStore_Store.get_metadata_basket(data)
# model.import_JSON_basket(data)
# print('getting is subscription')
# is_subscription_checkout_session = data[model.key_is_subscription]
code_currency = 'GBP' # data[model.key_code_currency]
print(f'currency code: {code_currency}')
except:
return jsonify({'status': 'failure', 'Message': 'Bad form data received by controller'})
items = []
for item in model.basket.items:
permutation = item.product.get_permutation_selected()
price = permutation.get_price_from_code_currency(code_currency)
items.append({'price': price.id_stripe_price,
'quantity': item.quantity })
# if is_subscription_checkout_session:
# break
print(f'items = {items}')
try:
# Create new Checkout Session for the order
# Other optional params include:
# [billing_address_collection] - to display billing address details on the page
# [customer] - if you have an existing Stripe Customer ID
# [payment_intent_data] - lets capture the payment later
# [customer_email] - lets you prefill the email input in the form
# [automatic_tax] - to automatically calculate sales tax, VAT and GST in the checkout page
# For full details see https://stripe.com/docs/api/checkout/sessions/create
stripe.api_key = model.key_secret_stripe
# ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
checkout_session = stripe.checkout.Session.create(
success_url=Model_View_Store_Checkout.url_host + '/store/checkout_success%3Fsession_id={CHECKOUT_SESSION_ID}',
cancel_url=Model_View_Store_Checkout.url_host + '/store/checkout_cancelled',
mode='subscription' if False else 'payment', # is_subscription_checkout_session
# automatic_tax={'enabled': True},
line_items=items
)
data_out = {}
# data_out['Success'] = True
data_out[f'{model.key_id_checkout}'] = checkout_session.id
data_out[f'{model.key_url_checkout}'] = checkout_session.url
# return jsonify(Success = True, data = data_out) # Success = True, f'{model.key_id_checkout}' = checkout_session.id)
print(f'checkout session url: {checkout_session.url}')
# redirect(checkout_session.url) # , code=303)
return jsonify(Success = True, data = data_out)
# return get_checkout_session(checkout_session.id)
except Exception as e: except Exception as e:
return jsonify(error=str(e)), 403 return jsonify(error=str(e)), 403
@app.route('/store/checkout_success?<session_id>', methods=['GET']) @app.route('/services', methods=['GET', 'POST'])
def checkout_success(session_id): # @app.route('/public_html/services', methods=['GET', 'POST'])
_m = 'store checkout success' def services():
# av.full_val_int(session_id, 'session_id', _m) try:
av.val_str(session_id, 'session_id', _m) model = Model_View_Home(db, get_info_user(), app)
if (session_id[:len('session_id=')] == 'session_id='): html_body = render_template('_page_services.html', model = model)
session_id = session_id[len('session_id='):] except Exception as e:
print(f'url var normal session id: {session_id}') return jsonify(error=str(e)), 403
print(f'{_m}\nstarting...') return html_body
id = request.args.get('sessionId')
print(f'request.args checkout session id: {id}')
checkout_session = stripe.checkout.Session.retrieve(session_id) # snore
print(f'checkout session data: {checkout_session}') @app.route('/license', methods=['GET'])
def license():
# validate billing information try:
model = Model_View_Home(db, get_info_user(), app)
html_body = render_template('_page_license.html', model = model)
model = Model_View_Store_Checkout_Success(db, get_info_user(), app) except Exception as e:
return str(e)
return render_template('_page_store_checkout_success.html', model=model) return html_body
@app.route('/accessibility-statement', methods=['GET'])
@app.route('/store/checkout_cancelled', methods=['GET']) def accessibility_statement():
def checkout_cancelled(): try:
_m = 'store checkout success' model = Model_View_Home(db, get_info_user(), app)
print(f'{_m}\nstarting...') html_body = render_template('_page_accessibility_statement.html', model = model)
return render_template('_page_store_checkout_cancelled.html', model=Model_View_Store_Checkout(db, get_info_user(), app)) except Exception as e:
return str(e)
return html_body
# include VAT in prices? @app.route('/accessibility-report', methods=['GET'])
@app.route('/store/set_is_included_VAT', methods=['POST']) def accessibility_report():
def set_is_included_VAT(): try:
_m = 'set_is_included_VAT' model = Model_View_Home(db, get_info_user(), app)
print(f'{_m}\nstarting...') html_body = render_template('_page_accessibility_report.html', model = model)
data = request.json except Exception as e:
print(f'data={data}') return str(e)
app.is_included_VAT = not app.is_included_VAT # session[app.KEY_IS_INCLUDED_VAT] # data[app.KEY_IS_INCLUDED_VAT] return html_body
return jsonify(Success=True, data={Model_View_Base.KEY_IS_INCLUDED_VAT: app.is_included_VAT}) @app.route('/retention-schedule', methods=['GET'])
def retention_schedule():
# delivery region try:
@app.route('/store/set_delivery_region', methods=['POST']) model = Model_View_Home(db, get_info_user(), app)
def set_delivery_region(): html_body = render_template('_page_retention_schedule.html', model = model)
_m = 'set_delivery_region' except Exception as e:
print(f'{_m}\nstarting...') return str(e)
data = request.json return html_body
print(f'data={data}') @app.route('/privacy-notice', methods=['GET'])
# model = Model_View_Store(db, get_info_user(), app) def privacy_notice():
form_data = data[Model_View_Store.key_form] try:
print(f'form_data: {form_data}') model = Model_View_Home(db, get_info_user(), app)
""" html_body = render_template('_page_privacy_notice.html', model = model)
form = Form_Delivery_Region(**form_data) except Exception as e:
print('form acquired') return str(e)
print(form.__repr__) return html_body
if form.validate_on_submit():
app.id_region_delivery = form.id_region_delivery.data
"""
id_region_delivery = form_data[Model_View_Store.KEY_BASKET][Model_View_Base.KEY_ID_REGION_DELIVERY]
print(f'id_region_delivery: {id_region_delivery}')
return jsonify(Success=True, data={Model_View_Base.KEY_ID_REGION_DELIVERY: id_region_delivery})
# currency
@app.route('/store/set_currency', methods=['POST'])
def set_currency():
_m = 'set_currency'
print(f'{_m}\nstarting...')
data = request.json
print(f'data={data}')
# model = Model_View_Store(db, get_info_user(), app)
form_data = data[Model_View_Store.key_form]
print(f'form_data: {form_data}')
"""
form = Form_Currency(**form_data)
print('form acquired')
print(form.__repr__)
if form.validate_on_submit():
app.id_currency = form.id_currency.data
print(f'id_currency: {app.id_currency}')
"""
app.id_currency = form_data[Model_View_Base.KEY_ID_CURRENCY]
print(f'id_currency: {app.id_currency}')
return jsonify(Success=True, data={Model_View_Base.KEY_ID_CURRENCY: app.id_currency})
# User authentication
@app.route("/login")
def login():
print(f'redirect uri: {url_for("login_callback", _external=True)}')
return oauth.auth0.authorize_redirect(
redirect_uri=url_for("login_callback", _external=True)
)
@app.route("/login_callback", methods=["GET", "POST"])
def login_callback():
token = oauth.auth0.authorize_access_token()
session[app.ID_TOKEN_USER] = token
# import user id
print(f'str(type(token)) = {str(type(token))}')
print(f'token = {token}')
userinfo = token.get('userinfo')
print(f'user info: {userinfo}')
# id_user = token.get('sub')
id_user = userinfo.get('sub')
print(f'user ID: {id_user}')
# id_user = get_id_user()
# add user to database
# DataStore_Store(db, userinfo).add_new_user(id_user) # this is part of get basket - should occur on page load
return redirect("/")
@app.route("/logout")
def logout():
session.clear()
return redirect(
"https://" + app.DOMAIN_AUTH0
+ "/v2/logout?"
+ urlencode(
{
"returnTo": url_for("home", _external=True),
"client_id": app.ID_AUTH0_CLIENT,
},
quote_via=quote_plus,
)
)
def get_info_user(): def get_info_user():
try: try:
@@ -680,48 +250,7 @@ def get_info_user():
except: except:
return {'sub': ''} return {'sub': ''}
"""
@app.route('/send-email', methods=['GET'])
def send_email():
try:
msg = Message("Flask Mail test", recipients=[app.config['MAIL_DEFAULT_SENDER']])
msg.body = "Dear Lord Edward Middleton-Smith,\n\nThis is a test email sent from Flask.\n\nKind regards,\nBot"
mail.send(msg)
except:
return "<html>Error</html>"
return "<html>Email sent</html>"
"""
"""
@app.route('/test-mysql', methods=['GET'])
def test_mysql():
model = Model_View_Store(db, get_info_user(), app)
_m = 'test_mysql'
proc_string = f'CALL {proc_name}('
if has_arguments:
arg_keys = list(argument_dict_list.keys())
for i in range(len(arg_keys)):
proc_string += f'{"" if i == 0 else ", "}:{arg_keys[i]}'
proc_string += ')'
proc_string = text(proc_string)
print(f'{_m}\nproc_string: {proc_string}\nargs: {argument_dict_list}')
# with self.db.session.begin() as session:
if has_arguments:
result = self.db.session.execute(proc_string, argument_dict_list)
else:
result = self.db.session.execute(proc_string)
print(f'result: {result}')
return "<html>MySQL test</html>"
"""
@app.route('/public_html/403.shtml', methods=['GET'])
def error_403():
return "<html>Error 403</html>"
# Onload # Onload
if True or __name__ == '__main__': if __name__ == '__main__':
app.run() app.run()
# app.run(debug=True, host="0.0.0.0", port=5000) # app.run(debug=True, host="0.0.0.0", port=5000)

205
app2.py
View File

@@ -1,205 +0,0 @@
"""
Project: PARTS Website
Author: Edward Middleton-Smith
Precision And Research Technology Systems Limited
Technology: App General
Feature: App
Description:
Initializes the Flask application, sets the configuration based on the environment, and defines two routes (/ and /about) that render templates with the specified titles.
"""
# IMPORTS
# VARIABLE INSTANTIATION
# METHODS
# IMPORTS
# internal
from config import app_config, Config
# from routes import bp_home
from forms import Form_Contact
from models.model_view_base import Model_View_Base
from models.model_view_home import Model_View_Home
from models.model_view_contact import Model_View_Contact
from business_objects.product import Product # , Product_Image_Filters, Resolution_Level_Enum
import lib.argument_validation as av
# external
from flask import Flask, render_template, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail, Message
from flask_wtf.csrf import CSRFProtect
import stripe
import json
from dotenv import load_dotenv, find_dotenv
import os
import sys
from urllib.parse import quote_plus, urlencode
from authlib.integrations.flask_client import OAuth
import jwt
# VARIABLE INSTANTIATION
app = Flask(__name__)
csrf = CSRFProtect(app)
CORS(app)
# app.register_blueprint(bp_home, url_prefix='')
email_recipient = 'edward.middletonsmith@gmail.com' # 'noreply'
# app.config.from_object(app_config)
# app.app_config = app_config
app.DEBUG = Config.DEBUG
app.TESTING = Config.TESTING
app.SECRET_KEY = "007cfbdaaf6d1eb27209720e8a5fc8ba0a334ae0be6fcac132b0a471549cde7c" # Config.SECRET_KEY
app.config['SECRET_KEY'] = app.SECRET_KEY
app.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI
app.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS
app.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT
app.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET
app.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0
app.ID_TOKEN_USER = Config.ID_TOKEN_USER
app.config.SQLALCHEMY_DATABASE_URI = Config.SQLALCHEMY_DATABASE_URI
app.config.SQLALCHEMY_TRACK_MODIFICATIONS = Config.SQLALCHEMY_TRACK_MODIFICATIONS
app.config.ID_AUTH0_CLIENT = Config.ID_AUTH0_CLIENT
app.config.ID_AUTH0_CLIENT_SECRET = Config.ID_AUTH0_CLIENT_SECRET
app.config.DOMAIN_AUTH0 = Config.DOMAIN_AUTH0
app.config.ID_TOKEN_USER = Config.ID_TOKEN_USER
app.MAIL_SERVER = Config.MAIL_SERVER
app.MAIL_PORT = Config.MAIL_PORT
app.MAIL_USE_TLS = Config.MAIL_USE_TLS
app.MAIL_USERNAME = Config.MAIL_USERNAME
app.MAIL_PASSWORD = Config.MAIL_PASSWORD
app.MAIL_DEFAULT_SENDER = Config.MAIL_DEFAULT_SENDER
app.config['MAIL_SERVER'] = Config.MAIL_SERVER
app.config['MAIL_PORT'] = Config.MAIL_PORT
app.config['MAIL_USE_TLS'] = Config.MAIL_USE_TLS
app.config['MAIL_USERNAME'] = Config.MAIL_USERNAME
app.config['MAIL_PASSWORD'] = Config.MAIL_PASSWORD
app.config['MAIL_DEFAULT_SENDER'] = Config.MAIL_DEFAULT_SENDER
app.RECAPTCHA_PUBLIC_KEY = Config.RECAPTCHA_PUBLIC_KEY
app.config['RECAPTCHA_PUBLIC_KEY'] = Config.RECAPTCHA_PUBLIC_KEY
app.RECAPTCHA_PRIVATE_KEY = Config.RECAPTCHA_PRIVATE_KEY
app.config['RECAPTCHA_PRIVATE_KEY'] = Config.RECAPTCHA_PRIVATE_KEY
# db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = app.SQLALCHEMY_DATABASE_URI
db = SQLAlchemy()
db.init_app(app)
with app.app_context():
db.create_all()
db.engine.url = app.config.SQLALCHEMY_DATABASE_URI
"""
oauth = OAuth(app)
oauth.register(
"auth0",
client_id = app.ID_AUTH0_CLIENT,
client_secret = app.ID_AUTH0_CLIENT_SECRET, # =env.get("AUTH0_CLIENT_SECRET"),
client_kwargs={
"scope": "openid profile email",
},
server_metadata_url=f'https://{app.DOMAIN_AUTH0}/.well-known/openid-configuration'
)
# session[app.ID_TOKEN_USER] = {'userinfo': {'sub': ''}}
"""
mail = Mail(app)
# METHODS
sys.path.insert(0, os.path.dirname(__file__))
@app.route('/hello')
def hello():
return "Hello, World!" + f"{app.errors}\n{app.config.SQLALCHEMY_DATABASE_URI == app.SQLALCHEMY_DATABASE_URI}"
@app.route('/goodbye')
def goodbye():
return "Goodbye, cruel World!"
"""
@app.route('/public_html/403.shtml', methods=['GET'])
def forbidden():
return send_from_directory('/home/partsltd/public_html', '403.shtml')
"""
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
message = 'It works!\n'
version = 'Python %s\n' % sys.version.split()[0]
response = '\n'.join([message, version])
return [response.encode()]
# ROUTING
@app.route('/', methods=['GET'])
def home():
try:
model = Model_View_Home(db, get_info_user(), app)
html_body = render_template('_page_home.html', model = model)
except Exception as e:
return str(e)
return html_body
@app.route('/contact', methods=['GET'])
def contact():
try:
form = Form_Contact()
model = Model_View_Contact(db, get_info_user(), app, form)
html_body = render_template('_page_contact.html', model = model)
except Exception as e:
return jsonify(error=str(e)), 403
return html_body
@app.route('/contact', methods=['POST'])
def contact_post():
try:
form = Form_Contact()
if form.validate_on_submit():
# Handle form submission
email = form.email.data
CC = form.CC.data # not in use
name = form.name.data
msg = form.msg.data
# send email
mailItem = Message("PARTS Website Contact Us Message", recipients=[app.config['MAIL_DEFAULT_SENDER']])
mailItem.body = f"Dear Lord Edward Middleton-Smith,\n\n{msg}\n\nKind regards,\n{name}\n{email}"
mail.send(mailItem)
return "Submitted."
return "Invalid. Failed to submit."
# html_body = render_template('_page_contact.html', model = model)
except Exception as e:
return jsonify(error=str(e)), 403
@app.route('/services', methods=['GET', 'POST'])
# @app.route('/public_html/services', methods=['GET', 'POST'])
def services():
try:
model = Model_View_Home(db, get_info_user(), app)
html_body = render_template('_page_services.html', model = model)
except Exception as e:
return jsonify(error=str(e)), 403
return html_body
@app.route('/license', methods=['GET'])
def license():
try:
model = Model_View_Home(db, get_info_user(), app)
html_body = render_template('_page_license.html', model = model)
except Exception as e:
return str(e)
return html_body
def get_info_user():
try:
return session[app.ID_TOKEN_USER].get('userinfo') # .get('sub')
except:
return {'sub': ''}
# Onload
if __name__ == '__main__':
app.run()
# app.run(debug=True, host="0.0.0.0", port=5000)

View File

@@ -40,10 +40,10 @@ class Config:
MAIL_SERVER = 'smtp.gmail.com' MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 587 MAIL_PORT = 587
MAIL_USE_TLS = True MAIL_USE_TLS = True
MAIL_USERNAME = 'edward.middletonsmith@gmail.com' MAIL_USERNAME = os.getenv('MAIL_DEFAULT_SENDER')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = 'edward.middletonsmith@gmail.com' MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
MAIL_CONTACT_PUBLIC = os.getenv('MAIL_CONTACT_PUBLIC')
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY') RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY') RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')

View File

@@ -24,7 +24,7 @@ class Form_Contact(FlaskForm):
email = StringField('Email address') email = StringField('Email address')
CC = BooleanField('Would you like to receive a copy of this email request?') # not in use CC = BooleanField('Would you like to receive a copy of this email request?') # not in use
name = StringField('Name') name = StringField('Name')
msg = TextAreaField('Message') message = TextAreaField('Message')
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
submit = SubmitField('Submit') submit = SubmitField('Submit')
@@ -32,7 +32,7 @@ class Form_Register(FlaskForm):
email = StringField('Email address') email = StringField('Email address')
CC = BooleanField('Would you like to receive a copy of this email request?') # not in use CC = BooleanField('Would you like to receive a copy of this email request?') # not in use
name = StringField('Name') name = StringField('Name')
msg = TextAreaField('Message') message = TextAreaField('Message')
submit = SubmitField('Submit') submit = SubmitField('Submit')

View File

@@ -57,6 +57,7 @@ class Model_View_Base(ABC):
FLAG_SCROLLABLE = 'scrollable' FLAG_SCROLLABLE = 'scrollable'
FLAG_SUBMITTED = 'submitted' FLAG_SUBMITTED = 'submitted'
# flagIsDatePicker = 'is-date-picker' # flagIsDatePicker = 'is-date-picker'
HASH_PAGE_ACCESSIBILITY_STATEMENT = '/accessibility-statement'
HASH_PAGE_CONTACT = '/contact' HASH_PAGE_CONTACT = '/contact'
HASH_PAGE_ERROR_NO_PERMISSION = '/error' HASH_PAGE_ERROR_NO_PERMISSION = '/error'
HASH_PAGE_HOME = '/' HASH_PAGE_HOME = '/'
@@ -78,6 +79,7 @@ class Model_View_Base(ABC):
ID_NAV_STORE_PRODUCT = 'navStoreProduct' ID_NAV_STORE_PRODUCT = 'navStoreProduct'
ID_OVERLAY_HAMBURGER = 'overlayHamburger' ID_OVERLAY_HAMBURGER = 'overlayHamburger'
ID_PAGE_BODY = 'pageBody' ID_PAGE_BODY = 'pageBody'
NAME_COMPANY = 'Precision And Research Technology Systems Limited'
URL_HOST = 'https://www.partsltd.co.uk' # 'http://127.0.0.1:5000' URL_HOST = 'https://www.partsltd.co.uk' # 'http://127.0.0.1:5000'
URL_GITHUB = 'https://github.com/Teddy-1024' URL_GITHUB = 'https://github.com/Teddy-1024'
URL_LINKEDIN = 'https://uk.linkedin.com/in/lordteddyms' URL_LINKEDIN = 'https://uk.linkedin.com/in/lordteddyms'

View File

@@ -12,4 +12,4 @@ def application(environ, start_response):
response = '\n'.join([message, version]) response = '\n'.join([message, version])
return [response.encode()] return [response.encode()]
from app2 import app as application # Import the Flask application from app.py from app import app as application # Import the Flask application from app.py

View File

@@ -464,7 +464,7 @@ button, .btn-submit, input[type="submit"] {
color: var(--c_purple_dark); color: var(--c_purple_dark);
font-weight: bold; font-weight: bold;
font-size: 18px; font-size: 18px;
height: 27px; height: 18px;
} }
.hamburger > :hover { .hamburger > :hover {
background-color: var(--c_purple_light); background-color: var(--c_purple_light);
@@ -472,8 +472,14 @@ button, .btn-submit, input[type="submit"] {
} }
.hamburger > * > * { .hamburger > * > * {
width: 100%; width: 100%;
/*
margin-top: 4.5px; margin-top: 4.5px;
margin-bottom: 4.5px; margin-bottom: 4.5px;
*/
}
.hamburger > .container {
padding-top: 4.5px;
padding-bottom: 4.5px;
} }
li { li {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
<!doctype HTML><html lang="en"><head><title>public-website-of-precision-and-research-technology-systems-limited-report.html</title><meta charset="utf-8"><style>
table {
border-collapse: collapse;
width: 100%;
}
table,
td,
th {
border: 1px solid #3b3b3b;
}
td:not(:last-child),
th {
padding: 1em;
vertical-align: top;
text-align: left;
}
td:not([class]):last-child {
padding: 0 1em;
}
</style></head><body><div><main> <h1>Report</h1> <div><h2>About the Evaluation</h2> <dl><dt>Report Creator </dt><dd>Lord Edward Middleton-Smith </dd><dt>Evaluation Commissioner </dt><dd>Lord Edward Middleton-Smith </dd><dt>Evaluation date </dt><dd>Tue Apr 30 2024</dd></dl> </div> <div><h2>Executive Summary</h2> <div><span>Not provided</span></div> </div> <div><h2>Scope of the Evaluation</h2> <dl><dt>Website name </dt><dd>Public Website of Precision And Research Technology Systems Limited </dd><dt>Scope of the website </dt><dd>'All web content of the public mobile and desktop website of Precision And Research Technology Systems Limited located at https://www.partsltd.co.uk/ </dd><dt>WCAG Version </dt><dd>2.2 </dd><dt>Conformance target </dt><dd>AA </dd><dt>Accessibility support baseline </dt><dd>Google Chrome with NVDA, FireFox with NVDA, Ecosia mobile browser with TalkBack. </dd><dt>Additional evaluation requirements </dt><dd><span>Not provided</span></dd></dl> </div> <h2>Detailed Audit Results</h2> <h3>Summary</h3> <p>Reported on 55 of 55 WCAG 2.2 AA
Success Criteria.</p> <ul><li><span>41</span> <span>Passed</span></li><li><span>0</span> <span>Failed</span></li><li><span>0</span> <span>Cannot tell</span></li><li><span>14</span> <span>Not present</span></li><li><span>0</span> <span>Not checked</span></li></ul> <h3>All Results</h3> <h4>1 Perceivable</h4> <h5 id="guideline-11">1.1 Text Alternatives</h5> <table aria-labelledby="guideline-11"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-111">1.1.1: Non-text Content</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>All user input controls have descriptive names and v2 Google reCaptcha used with text label to identify to user that they must check the box to prove they are not a bot.</p>
</td> </tr></tbody> </table><h5 id="guideline-12">1.2 Time-based Media</h5> <table aria-labelledby="guideline-12"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-121">1.2.1: Audio-only and Video-only (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-122">1.2.2: Captions (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-123">1.2.3: Audio Description or Media Alternative (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-124">1.2.4: Captions (Live)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-125">1.2.5: Audio Description (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-13">1.3 Adaptable</h5> <table aria-labelledby="guideline-13"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-131">1.3.1: Info and Relationships</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Elements change in response to zoom and viewport dimensions properly. Aria-label provided for all images.</p>
</td> </tr><tr><th scope="row" id="criterion-132">1.3.2: Meaningful Sequence</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Flow layout keeps associated sections together but allows dynamic structure depending on size of elements relative to screen.</p>
</td> </tr><tr><th scope="row" id="criterion-133">1.3.3: Sensory Characteristics</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Content reads properly as raw text in default order. Aria-label provided for all images and names for all form input components.</p>
</td> </tr><tr><th scope="row" id="criterion-134">1.3.4: Orientation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Orientation of content is not locked.</p>
</td> </tr><tr><th scope="row" id="criterion-135">1.3.5: Identify Input Purpose</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Appropriate visible labels used alongside name attributes for form input elements.</p>
</td> </tr></tbody> </table><h5 id="guideline-14">1.4 Distinguishable</h5> <table aria-labelledby="guideline-14"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-141">1.4.1: Use of Color</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Colour only used to convey meaning for text hyperlinks, which have alt-text attributes to identify their purpose and presence.</p>
</td> </tr><tr><th scope="row" id="criterion-142">1.4.2: Audio Control</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-143">1.4.3: Contrast (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Webpage text contrast assessed with tool at this website: <a href="https://accessibleweb.com/color-contrast-checker/">https://accessibleweb.com/color-contrast-checker/</a></p>
</td> </tr><tr><th scope="row" id="criterion-144">1.4.4: Resize text</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>All webpages can be scaled to up to 500% while maintaining structure and visible components.</p>
</td> </tr><tr><th scope="row" id="criterion-145">1.4.5: Images of Text</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-1410">1.4.10: Reflow</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Flex layout forces only vertical scrolling at required viewport dimensions.</p>
</td> </tr><tr><th scope="row" id="criterion-1411">1.4.11: Non-text Contrast</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Strong borders used for form input components.</p>
</td> </tr><tr><th scope="row" id="criterion-1412">1.4.12: Text Spacing</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Content becomes vertically scrollable as necessary.</p>
</td> </tr><tr><th scope="row" id="criterion-1413">1.4.13: Content on Hover or Focus</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Hamburger menu button for navigation overlay used. The button remains stationary with no elements above it, throughout use of navigation.</p>
</td> </tr></tbody> </table><h4>2 Operable</h4> <h5 id="guideline-21">2.1 Keyboard Accessible</h5> <table aria-labelledby="guideline-21"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-211">2.1.1: Keyboard</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard. Other keyboard shortcuts not changed.</p>
</td> </tr><tr><th scope="row" id="criterion-212">2.1.2: No Keyboard Trap</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard.</p>
</td> </tr><tr><th scope="row" id="criterion-214">2.1.4: Character Key Shortcuts</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard. Other keyboard shortcuts not changed, and can be set by user's device settings.</p>
</td> </tr></tbody> </table><h5 id="guideline-22">2.2 Enough Time</h5> <table aria-labelledby="guideline-22"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-221">2.2.1: Timing Adjustable</th> <td> <p><span>Result:</span> Not present</p> </td> <td> <p>Observations:</p> <p>No session time limits imposed on user.</p>
</td> </tr><tr><th scope="row" id="criterion-222">2.2.2: Pause, Stop, Hide</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-23">2.3 Seizures and Physical Reactions</h5> <table aria-labelledby="guideline-23"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-231">2.3.1: Three Flashes or Below Threshold</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-24">2.4 Navigable</h5> <table aria-labelledby="guideline-24"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-241">2.4.1: Bypass Blocks</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-242">2.4.2: Page Titled</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Descriptive titles used on all webpages.</p>
</td> </tr><tr><th scope="row" id="criterion-243">2.4.3: Focus Order</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Keyboard tab indices set for logical navigation around pages.</p>
</td> </tr><tr><th scope="row" id="criterion-244">2.4.4: Link Purpose (In Context)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Descriptive aria-label provided for all text hyperlinks.</p>
</td> </tr><tr><th scope="row" id="criterion-245">2.4.5: Multiple Ways</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Navigation on all webpages and company logo links to home page.</p>
</td> </tr><tr><th scope="row" id="criterion-246">2.4.6: Headings and Labels</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-247">2.4.7: Focus Visible</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-2411">2.4.11: Focus Not Obscured (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-25">2.5 Input Modalities</h5> <table aria-labelledby="guideline-25"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-251">2.5.1: Pointer Gestures</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-252">2.5.2: Pointer Cancellation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Up event used to trigger events across website.</p>
</td> </tr><tr><th scope="row" id="criterion-253">2.5.3: Label in Name</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Input label for attributes used to associate with input elements.
Aria-label attributes used for text hyperlinks.</p>
</td> </tr><tr><th scope="row" id="criterion-254">2.5.4: Motion Actuation</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-257">2.5.7: Dragging Movements</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-258">2.5.8: Target Size (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Minimum control dimension is 27 CSS pixels.</p>
</td> </tr></tbody> </table><h4>3 Understandable</h4> <h5 id="guideline-31">3.1 Readable</h5> <table aria-labelledby="guideline-31"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-311">3.1.1: Language of Page</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>English - Great Britain on all pages.</p>
</td> </tr><tr><th scope="row" id="criterion-312">3.1.2: Language of Parts</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No language changes across website.</p>
</td> </tr></tbody> </table><h5 id="guideline-32">3.2 Predictable</h5> <table aria-labelledby="guideline-32"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-321">3.2.1: On Focus</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No change of context initiated except by button click to navigate to submit a form and/or navigate to a new page.</p>
</td> </tr><tr><th scope="row" id="criterion-322">3.2.2: On Input</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No change of context initiated except by button click to navigate to submit a form and/or navigate to a new page.</p>
</td> </tr><tr><th scope="row" id="criterion-323">3.2.3: Consistent Navigation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Navigation component and mechanisms shared across all pages.</p>
</td> </tr><tr><th scope="row" id="criterion-324">3.2.4: Consistent Identification</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Classes and CSS styles used to group collections of elements by purpose and functionality.</p>
</td> </tr><tr><th scope="row" id="criterion-326">3.2.6: Consistent Help</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Contact us button provided in a consistent format.</p>
</td> </tr></tbody> </table><h5 id="guideline-33">3.3 Input Assistance</h5> <table aria-labelledby="guideline-33"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-331">3.3.1: Error Identification</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Each input element has an associated error display label which are triggered when input validation is not met.</p>
</td> </tr><tr><th scope="row" id="criterion-332">3.3.2: Labels or Instructions</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Each user input has a descriptive label.</p>
</td> </tr><tr><th scope="row" id="criterion-333">3.3.3: Error Suggestion</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Text description of incomplete required fields is provided. Description of what caused error is issued when it involves simple regular expression validation.</p>
</td> </tr><tr><th scope="row" id="criterion-334">3.3.4: Error Prevention (Legal, Financial, Data)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-337">3.3.7: Redundant Entry</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No redundant data entry required. User's browser cache stores form data for rapid re-entry on crash or any other occasion.</p>
</td> </tr><tr><th scope="row" id="criterion-338">3.3.8: Accessible Authentication (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Alternative sign in methods provided, including recovery by code by phone and email.</p>
</td> </tr></tbody> </table><h4>4 Robust</h4> <h5 id="guideline-41">4.1 Compatible</h5> <table aria-labelledby="guideline-41"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-412">4.1.2: Name, Role, Value</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Aria-label attribute maintained for all images and text hyperlinks. Label for attribute used to associate input elements with descriptive labels.</p>
</td> </tr><tr><th scope="row" id="criterion-413">4.1.3: Status Messages</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Success feedback provided on form submission, with descriptive errors on failure.</p>
</td> </tr></tbody> </table> <h2>Sample of Audited Web Pages</h2> <ol><li><span>Home</span> - <span>https://www.partsltd.co.uk/</span> </li><li><span>Contact us</span> - <span>https://www.partsltd.co.uk/contact</span> </li><li><span>Services</span> - <span>https://www.partsltd.co.uk/services</span> </li><li><span></span> - <span></span> </li></ol> <h2>Web Technology</h2> <p>HTML,CSS,JavaScript,python Flask</p> <h2>Recording of Evaluation Specifics</h2> <p>Not provided</p> </main></div></body></html>

View File

@@ -0,0 +1,5 @@
var _loading = true;
function hookupPageAccessibilityStatement() {
_loading = false;
}

View File

@@ -1,5 +1,5 @@
var _loading = true; var _loading = true;
function hookupPageHome() { function hookupPageLicense() {
_loading = false; _loading = false;
} }

View File

@@ -0,0 +1,60 @@
<!doctype HTML><html lang="en"><head><title>public-website-of-precision-and-research-technology-systems-limited-report.html</title><meta charset="utf-8"><style>
table {
border-collapse: collapse;
width: 100%;
}
table,
td,
th {
border: 1px solid #3b3b3b;
}
td:not(:last-child),
th {
padding: 1em;
vertical-align: top;
text-align: left;
}
td:not([class]):last-child {
padding: 0 1em;
}
</style></head><body><div><main> <h1>Report</h1> <div><h2>About the Evaluation</h2> <dl><dt>Report Creator </dt><dd>Lord Edward Middleton-Smith </dd><dt>Evaluation Commissioner </dt><dd>Lord Edward Middleton-Smith </dd><dt>Evaluation date </dt><dd>Tue Apr 30 2024</dd></dl> </div> <div><h2>Executive Summary</h2> <div><span>Not provided</span></div> </div> <div><h2>Scope of the Evaluation</h2> <dl><dt>Website name </dt><dd>Public Website of Precision And Research Technology Systems Limited </dd><dt>Scope of the website </dt><dd>'All web content of the public mobile and desktop website of Precision And Research Technology Systems Limited located at https://www.partsltd.co.uk/ </dd><dt>WCAG Version </dt><dd>2.2 </dd><dt>Conformance target </dt><dd>AA </dd><dt>Accessibility support baseline </dt><dd>Google Chrome with NVDA, FireFox with NVDA, Ecosia mobile browser with TalkBack. </dd><dt>Additional evaluation requirements </dt><dd><span>Not provided</span></dd></dl> </div> <h2>Detailed Audit Results</h2> <h3>Summary</h3> <p>Reported on 55 of 55 WCAG 2.2 AA
Success Criteria.</p> <ul><li><span>41</span> <span>Passed</span></li><li><span>0</span> <span>Failed</span></li><li><span>0</span> <span>Cannot tell</span></li><li><span>14</span> <span>Not present</span></li><li><span>0</span> <span>Not checked</span></li></ul> <h3>All Results</h3> <h4>1 Perceivable</h4> <h5 id="guideline-11">1.1 Text Alternatives</h5> <table aria-labelledby="guideline-11"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-111">1.1.1: Non-text Content</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>All user input controls have descriptive names and v2 Google reCaptcha used with text label to identify to user that they must check the box to prove they are not a bot.</p>
</td> </tr></tbody> </table><h5 id="guideline-12">1.2 Time-based Media</h5> <table aria-labelledby="guideline-12"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-121">1.2.1: Audio-only and Video-only (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-122">1.2.2: Captions (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-123">1.2.3: Audio Description or Media Alternative (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-124">1.2.4: Captions (Live)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-125">1.2.5: Audio Description (Prerecorded)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-13">1.3 Adaptable</h5> <table aria-labelledby="guideline-13"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-131">1.3.1: Info and Relationships</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Elements change in response to zoom and viewport dimensions properly. Aria-label provided for all images.</p>
</td> </tr><tr><th scope="row" id="criterion-132">1.3.2: Meaningful Sequence</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Flow layout keeps associated sections together but allows dynamic structure depending on size of elements relative to screen.</p>
</td> </tr><tr><th scope="row" id="criterion-133">1.3.3: Sensory Characteristics</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Content reads properly as raw text in default order. Aria-label provided for all images and names for all form input components.</p>
</td> </tr><tr><th scope="row" id="criterion-134">1.3.4: Orientation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Orientation of content is not locked.</p>
</td> </tr><tr><th scope="row" id="criterion-135">1.3.5: Identify Input Purpose</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Appropriate visible labels used alongside name attributes for form input elements.</p>
</td> </tr></tbody> </table><h5 id="guideline-14">1.4 Distinguishable</h5> <table aria-labelledby="guideline-14"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-141">1.4.1: Use of Color</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Colour only used to convey meaning for text hyperlinks, which have alt-text attributes to identify their purpose and presence.</p>
</td> </tr><tr><th scope="row" id="criterion-142">1.4.2: Audio Control</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-143">1.4.3: Contrast (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Webpage text contrast assessed with tool at this website: <a href="https://accessibleweb.com/color-contrast-checker/">https://accessibleweb.com/color-contrast-checker/</a></p>
</td> </tr><tr><th scope="row" id="criterion-144">1.4.4: Resize text</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>All webpages can be scaled to up to 500% while maintaining structure and visible components.</p>
</td> </tr><tr><th scope="row" id="criterion-145">1.4.5: Images of Text</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-1410">1.4.10: Reflow</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Flex layout forces only vertical scrolling at required viewport dimensions.</p>
</td> </tr><tr><th scope="row" id="criterion-1411">1.4.11: Non-text Contrast</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Strong borders used for form input components.</p>
</td> </tr><tr><th scope="row" id="criterion-1412">1.4.12: Text Spacing</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Content becomes vertically scrollable as necessary.</p>
</td> </tr><tr><th scope="row" id="criterion-1413">1.4.13: Content on Hover or Focus</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Hamburger menu button for navigation overlay used. The button remains stationary with no elements above it, throughout use of navigation.</p>
</td> </tr></tbody> </table><h4>2 Operable</h4> <h5 id="guideline-21">2.1 Keyboard Accessible</h5> <table aria-labelledby="guideline-21"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-211">2.1.1: Keyboard</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard. Other keyboard shortcuts not changed.</p>
</td> </tr><tr><th scope="row" id="criterion-212">2.1.2: No Keyboard Trap</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard.</p>
</td> </tr><tr><th scope="row" id="criterion-214">2.1.4: Character Key Shortcuts</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Tab indices set to enable correct transition around page by keyboard. Other keyboard shortcuts not changed, and can be set by user's device settings.</p>
</td> </tr></tbody> </table><h5 id="guideline-22">2.2 Enough Time</h5> <table aria-labelledby="guideline-22"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-221">2.2.1: Timing Adjustable</th> <td> <p><span>Result:</span> Not present</p> </td> <td> <p>Observations:</p> <p>No session time limits imposed on user.</p>
</td> </tr><tr><th scope="row" id="criterion-222">2.2.2: Pause, Stop, Hide</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-23">2.3 Seizures and Physical Reactions</h5> <table aria-labelledby="guideline-23"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-231">2.3.1: Three Flashes or Below Threshold</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-24">2.4 Navigable</h5> <table aria-labelledby="guideline-24"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-241">2.4.1: Bypass Blocks</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-242">2.4.2: Page Titled</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Descriptive titles used on all webpages.</p>
</td> </tr><tr><th scope="row" id="criterion-243">2.4.3: Focus Order</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Keyboard tab indices set for logical navigation around pages.</p>
</td> </tr><tr><th scope="row" id="criterion-244">2.4.4: Link Purpose (In Context)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Descriptive aria-label provided for all text hyperlinks.</p>
</td> </tr><tr><th scope="row" id="criterion-245">2.4.5: Multiple Ways</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Navigation on all webpages and company logo links to home page.</p>
</td> </tr><tr><th scope="row" id="criterion-246">2.4.6: Headings and Labels</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-247">2.4.7: Focus Visible</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-2411">2.4.11: Focus Not Obscured (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> </td> </tr></tbody> </table><h5 id="guideline-25">2.5 Input Modalities</h5> <table aria-labelledby="guideline-25"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-251">2.5.1: Pointer Gestures</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-252">2.5.2: Pointer Cancellation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Up event used to trigger events across website.</p>
</td> </tr><tr><th scope="row" id="criterion-253">2.5.3: Label in Name</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Input label for attributes used to associate with input elements.
Aria-label attributes used for text hyperlinks.</p>
</td> </tr><tr><th scope="row" id="criterion-254">2.5.4: Motion Actuation</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-257">2.5.7: Dragging Movements</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-258">2.5.8: Target Size (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Minimum control dimension is 27 CSS pixels.</p>
</td> </tr></tbody> </table><h4>3 Understandable</h4> <h5 id="guideline-31">3.1 Readable</h5> <table aria-labelledby="guideline-31"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-311">3.1.1: Language of Page</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>English - Great Britain on all pages.</p>
</td> </tr><tr><th scope="row" id="criterion-312">3.1.2: Language of Parts</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No language changes across website.</p>
</td> </tr></tbody> </table><h5 id="guideline-32">3.2 Predictable</h5> <table aria-labelledby="guideline-32"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-321">3.2.1: On Focus</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No change of context initiated except by button click to navigate to submit a form and/or navigate to a new page.</p>
</td> </tr><tr><th scope="row" id="criterion-322">3.2.2: On Input</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No change of context initiated except by button click to navigate to submit a form and/or navigate to a new page.</p>
</td> </tr><tr><th scope="row" id="criterion-323">3.2.3: Consistent Navigation</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Navigation component and mechanisms shared across all pages.</p>
</td> </tr><tr><th scope="row" id="criterion-324">3.2.4: Consistent Identification</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Classes and CSS styles used to group collections of elements by purpose and functionality.</p>
</td> </tr><tr><th scope="row" id="criterion-326">3.2.6: Consistent Help</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Contact us button provided in a consistent format.</p>
</td> </tr></tbody> </table><h5 id="guideline-33">3.3 Input Assistance</h5> <table aria-labelledby="guideline-33"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-331">3.3.1: Error Identification</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Each input element has an associated error display label which are triggered when input validation is not met.</p>
</td> </tr><tr><th scope="row" id="criterion-332">3.3.2: Labels or Instructions</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Each user input has a descriptive label.</p>
</td> </tr><tr><th scope="row" id="criterion-333">3.3.3: Error Suggestion</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Text description of incomplete required fields is provided. Description of what caused error is issued when it involves simple regular expression validation.</p>
</td> </tr><tr><th scope="row" id="criterion-334">3.3.4: Error Prevention (Legal, Financial, Data)</th> <td> <p><span>Result:</span> Not present</p> </td> <td> </td> </tr><tr><th scope="row" id="criterion-337">3.3.7: Redundant Entry</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>No redundant data entry required. User's browser cache stores form data for rapid re-entry on crash or any other occasion.</p>
</td> </tr><tr><th scope="row" id="criterion-338">3.3.8: Accessible Authentication (Minimum)</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Alternative sign in methods provided, including recovery by code by phone and email.</p>
</td> </tr></tbody> </table><h4>4 Robust</h4> <h5 id="guideline-41">4.1 Compatible</h5> <table aria-labelledby="guideline-41"><tbody><tr><th scope="col">Success Criterion</th> <th scope="col">Result</th> <th scope="col">Observations</th> </tr> <tr><th scope="row" id="criterion-412">4.1.2: Name, Role, Value</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Aria-label attribute maintained for all images and text hyperlinks. Label for attribute used to associate input elements with descriptive labels.</p>
</td> </tr><tr><th scope="row" id="criterion-413">4.1.3: Status Messages</th> <td> <p><span>Result:</span> Passed</p> </td> <td> <p>Observations:</p> <p>Success feedback provided on form submission, with descriptive errors on failure.</p>
</td> </tr></tbody> </table> <h2>Sample of Audited Web Pages</h2> <ol><li><span>Home</span> - <span>https://www.partsltd.co.uk/</span> </li><li><span>Contact us</span> - <span>https://www.partsltd.co.uk/contact</span> </li><li><span>Services</span> - <span>https://www.partsltd.co.uk/services</span> </li><li><span></span> - <span></span> </li></ol> <h2>Web Technology</h2> <p>HTML,CSS,JavaScript,python Flask</p> <h2>Recording of Evaluation Specifics</h2> <p>Not provided</p> </main></div></body></html>

View File

@@ -0,0 +1,211 @@
{% extends 'layout.html' %}
{% block title %}{{ model.title }}{% endblock %}
{% block page_body %}
<!-- Include Stylesheet -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/services.css') }}">
<!-- HTML content -->
<div class="{{ model.FLAG_CARD }} {{ model.FLAG_COLUMN }}">
<h4>Accessibility statement for {{ model.NAME_COMPANY }}</h4>
<!--
[Note: you can have a single accessibility statement that covers multiple domains, or a separate statement for each domain or subdomain. As long as the user can access relevant accessibility information easily from any page on your website.
For mobile apps also include the applications version information and publication date.
This wording is legally required and must not be changed.]
-->
<p>This accessibility statement applies to the public website of {{ model.NAME_COMPANY }}.</p>
<!--
[Note: use this section to make a brief, general statement about what the website allows disabled users to do. Base it on the evaluation covered in detail in the Technical information about this websites accessibility section.
It must be specific to your own website. If youre not confident that something is accurate, leave it out. If youre not confident enough to say anything specific here, leave this section out completely.]
-->
<p>This website is run by {{ model.NAME_COMPANY }}. We want as many people as possible to be able to use this website. For example, that means you should be able to:
<ul>
<li>zoom in up to 400% without the text spilling off the screen</li>
<li>navigate most of the website using just a keyboard or speech recognition software</li>
<li>listen to most of the website using a screen reader (including the most recent versions of NVDA and TalkBack)</li>
<li>We've also made the website text as simple as possible to understand.</li>
<a href="https://mcmw.abilitynet.org.uk/">AbilityNet</a> has advice on making your device easier to use if you have a disability.
</p>
<h5>How accessible this website is</h5>
<!--
[Note: use this section to provide a summary of accessibility issues that a disabled user can act on - for example, avoid a particular section of the website, or request an alternative version rather than waste time trying to make it work with their assistive technology. Try to list in order of most impact to least impact.]
-->
<p>We know some parts of this website are not fully accessible:
<ul>
<li>you cannot modify the line height or spacing of text</li>
</ul>
</p>
<h5>Feedback and contact information</h5>
<p>If you find any problems not listed on this page or think were not meeting accessibility requirements, contact: {{ model.app.MAIL_CONTACT_PUBLIC }} or complete our Contact Us form.
If you need information on this website in a different format like accessible PDF, large print, easy read, audio recording or braille:
{% set block_id = 'button_get_in_touch' %}
{% include '_shared.html' %}
Well consider your request and get back to you in 7 days.
<!-- If you cannot view the map on our contact us page, email us for directions. -->
</p>
<h5>Enforcement procedure</h5>
<p>The Equality and Human Rights Commission (EHRC) is responsible for enforcing the Public Sector Bodies (Websites and Mobile Applications) (No. 2) Accessibility Regulations 2018 (the accessibility regulations). If youre not happy with how we respond to your complaint, contact the <a href="https://www.equalityadvisoryservice.com/">Equality Advisory and Support Service (EASS).</a>
</p>
<!--
[Note: if your organisation is based in Northern Ireland, refer users who want to complain to the Equality Commission for Northern Ireland (ECNI) instead of the EASS and EHRC.
You must link to either the EASS or ECNI websites.]
-->
<h4>Technical information about this websites accessibility</h4>
<!--
[Note: this wording is legally required and must not be changed.]
-->
<p>{{ model.NAME_COMPANY }} is committed to making its website accessible, in accordance with the Public Sector Bodies (Websites and Mobile Applications) (No. 2) Accessibility Regulations 2018.
</p>
<h5>Compliance status</h5>
<p>The website has been tested against the Web Content Accessibility Guidelines (WCAG) 2.2 AA standard.
</p>
<!--
[Note: say that the website is fully compliant if the website meets the Web Content Accessibility Guidelines (WCAG) [2.1 or 2.2] AA standard in full.
Say that its partially compliant if it meets most requirements of the WCAG [2.1 or 2.2] AA standard.
If it does not meet most requirements of the WCAG [2.1 or 2.2] AA standard, say that its not compliant.
If your website is either partially compliant or not compliant with the WCAG [2.1 or 2.2] AA standard, youll need to explain why. This will be due to one or both of the following:
<ul>
<li>non-compliances - this means the content in question is in scope of the regulations, but theres an accessibility problem with it</li>
<li>an exemption - this means the inaccessible content is out of scope of the regulations, or itd be a disproportionate burden for you to make it accessible</li>
</ul>
Theres a legally required way of expressing the compliance status of your website, so do not change it. Choose one of the options below, for example (a), (b) or (c), and delete those not applicable.
-->
<p><!-- [Select (a) only if all requirements of the technical specification are fully met without exceptions for WCAG 2.1 or WCAG 2.2.] -->
(a) This website is fully compliant with the <!--[Web Content Accessibility Guidelines version 2.1 AA standard or -->Web Content Accessibility Guidelines version 2.2 AA standard.
<!--
[Select (b) if most requirements of the technical specification are met, but with some exceptions. This means not yet fully compliant and that the necessary measures are to be taken in order to reach full compliance.]
(b) This website is partially compliant with the [Web Content Accessibility Guidelines version 2.1 AA standard or Web Content Accessibility Guidelines version 2.2 AA standard], due to [insert one of the following: the non-compliances, the exemptions or the non-compliances and exemptions] listed below.
[Select (c) if most requirements of the technical specification are not met.]
(c) This website is not compliant with the [Web Content Accessibility Guidelines version 2.1 AA standard or Web Content Accessibility Guidelines version 2.2 AA standard]. The [insert one of the following: non-compliances, exemptions or non-compliances and exemptions] are listed below.
-->
</p>
<!--
<h4>Non-accessible content</h4>
<!--
[Note: if the website is fully compliant with the standard, you can leave the Non-accessible content section out.
Otherwise, do not change the Non-accessible content heading or the The content listed below is non-accessible for the following reasons sentence - theyre legally required.
Do not change the Non-compliance with the accessibility regulations, Disproportionate burden and Content thats not within the scope of the accessibility regulations subheadings: theyre also legally required.
But if you need to list a lot of problems, you can break these subsections up with further subheadings - for example, Navigation and accessing information or Interactive tools and transactions.]
--
<p>The content listed below is non-accessible for the following reasons.
</p>
<h5>Non-compliance with the accessibility regulations</h5>
<!--
[Note: In this subsection, list:
<ul>
<li>accessibility problems</li>
<li>which of the WCAG success criteria the problem fails on</li>
<li>when you plan to fix the problem</li>
</ul>
Do not include any problems where youre claiming disproportionate burden, or where the problem is outside the scope of the accessibility regulations (those should go in the subsections below).
These are examples only. Your statement must be specific to your website.]
--
<p>Some images do not have a text alternative, so people using a screen reader cannot access the information. This fails WCAG 2.2 success criterion 1.1.1 (non-text content).
We plan to add text alternatives for all images by September 2022. When we publish new content well make sure our use of images meets accessibility standards.
</p>
<h5>Disproportionate burden</h5>
<!--
[Note: in this subsection list accessibility problems youre claiming would be a disproportionate burden to fix.
You must carry out an assessment before claiming disproportionate burden.
Bear in mind that something which is a disproportionate burden now will not necessarily be a disproportionate burden forever. If the circumstances change, your ability to claim disproportionate burden may change too.
These are examples only. Your statement should be specific to your website.]
--
<p><strong>Navigation and accessing information</strong>
Theres no way to skip the repeated content in the page header (for example, a skip to main content option).
Its not always possible to change the device orientation from horizontal to vertical without making it more difficult to view the content.
Its not possible for users to change text size without some of the content overlapping.
Weve assessed the cost of fixing the issues with navigation and accessing information. We believe that doing so now would be a disproportionate burden within the meaning of the accessibility regulations. We will make another assessment when the supplier contract is up for renewal, likely to be in [rough timing].
</p>
<h5>Content thats not within the scope of the accessibility regulations</h5>
<!--
[Note: in this subsection list accessibility problems that fall outside the scope of the accessibility regulations.]
--
<p><strong>PDFs and other documents</strong>
The accessibility regulations do not require us to fix PDFs or other documents published before 23 September 2018 if theyre not essential to providing our services. For example, we do not plan to fix [example of non-essential document].
Any new PDFs or Word documents we publish will meet accessibility standards.
<strong>Live video</strong>
We do not plan to add captions to live video streams because live video is exempt from meeting the accessibility regulations.
</p>
-->
<!--
<h4>What were doing to improve accessibility</h4>
<!--
[Note: publishing an accessibility roadmap is optional. Its a good idea to publish one if you want to be specific about the order youre planning to tackle accessibility issues, and theres no space to do so in the accessibility statement itself.]
--
<p>Our accessibility roadmap [add link to roadmap] shows how and when we plan to improve accessibility on this website.
</p>
-->
<h4>Preparation of this accessibility statement</h4>
<!--
[Note: the wording about when the statement was prepared and reviewed is required.
It is recommended that an audit be carried out following a substantial revision to your website. The statement must also be updated.
The statement must be reviewed at least once a year, even if there have not been significant changes to the website. Include the date of the last review.]
-->
<p>This statement was prepared on 30/04/2024<!--[date when it was first published]-->. It was last reviewed on 30/04/2024<!--[date when it was last reviewed]-->.
This website was last tested on 30/04/2024 against the WCAG 2.2 AA standard.
</p>
<!--
[Note: describe how you tested the website to write this statement - such as a self-assessment done by the website team or an assessment carried out by a third party.]
-->
<p>The test was carried out by <!--[add name of organisation that carried out test, or indicate that you did your own testing]-->{{ model.NAME_COMPANY }}. All pages were tested using automated testing tools by our website team. A further audit of the website was carried out to the WCAG 2.2 AA standard.</p>
<!--
[Note: publishing the test report is optional but doing so may allow you to make your accessibility statement shorter and more focused.]
https://www.w3.org/WAI/eval/report-tool/#/
-->
<p>You can read the full accessibility test report {{ url_for('accessibility_report') }}.</p>
</div>
<!-- Include JavaScript -->
<script src="{{ url_for('static', filename='js/accessibility_statement.js') }}"></script>
<script>
var hashPageCurrent = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
$(document).ready(function() {
hookupPageAccessibilityStatement();
});
</script>
{% endblock %}

View File

@@ -55,8 +55,8 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="container-input"> <div class="container-input">
{{ model.form.msg.label }} {{ model.form.message.label }}
{{ model.form.msg(rows=4, cols=80) }} {{ model.form.message(rows=4, cols=80) }}
{% for error in model.form.name.errors %} {% for error in model.form.name.errors %}
<p class="error">{{ error }}</p> <p class="error">{{ error }}</p>
{% endfor %} {% endfor %}
@@ -75,7 +75,7 @@
<h3 class="label-title" style="padding-bottom: 0;">Where to find us</h3> <h3 class="label-title" style="padding-bottom: 0;">Where to find us</h3>
</div> </div>
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_ROW }}"> <div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_ROW }}">
<h4>edward.middletonsmith@gmail.com</h4> <h4>{{ model.app.MAIL_CONTACT_PUBLIC }}</h4>
</div> </div>
<div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_ROW }}" style="padding-top: 0; width: fit-content;"> <div class="{{ model.FLAG_CONTAINER }} {{ model.FLAG_ROW }}" style="padding-top: 0; width: fit-content;">
<a href="{{ model.url_LinkedIn }}" class="container-icon-label"><img class="img-icon" src="{{ url_for('static', filename='images/Logo_LinkedIn.png') }}" alt="LinkedIn"></img><!--<h4>LinkedIn</h4>--></a> <a href="{{ model.url_LinkedIn }}" class="container-icon-label"><img class="img-icon" src="{{ url_for('static', filename='images/Logo_LinkedIn.png') }}" alt="LinkedIn"></img><!--<h4>LinkedIn</h4>--></a>

View File

@@ -11,7 +11,8 @@
<!-- HTML content --> <!-- HTML content -->
<div> <div>
<p style="font-size: 24px; color: white;">Your software engineering solution</p> <p style="font-size: 24px; color: white;">Your software engineering solution</p>
<button class="button-contact">Get in touch</button> {% set block_id = 'button_get_in_touch' %}
{% include '_shared.html' %}
</div> </div>
<!-- Include JavaScript --> <!-- Include JavaScript -->

View File

@@ -0,0 +1,92 @@
{% extends 'layout.html' %}
{% block title %}{{ model.title }}{% endblock %}
{% block page_body %}
<!-- Include Stylesheet -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/services.css') }}">
<!-- HTML content -->
<div class="{{ model.FLAG_CARD }} {{ model.FLAG_COLUMN }}">
<h4>Generated privacy notice - general business</h4>
<h5>Precision and Research Technology Systems Ltd customer privacy notice</h5>
<p>This privacy notice tells you what to expect us to do with your personal information.</p>
<h5>Our contact details</h5>
<p>Email</p>
<p>teddy@partsltd.co.uk</p>
<h5>What information we collect, use, and why</h5>
<p>We collect or use the following information to provide services and goods, including delivery:</p>
<ul>
<li>Names and contact details</li>
</ul>
<h5>Lawful bases</h5>
<p>Our lawful bases for collecting or using personal information to provide services and goods are:</p>
<ul>
<li>Legitimate interest:</li>
<ul>
<li>
I collect the user's name and email address alongside a message they send on our 'Contact Us' form in order to respond to their request. Alternatively, the user has the option to contact us directly by email.
</li>
</ul>
</ul>
<h5>Where we get personal information from</h5>
<ul>
<li>People directly</li>
</ul>
<h5>How long we keep information</h5>
<p>For information on how long we keep personal information, see our retention schedule at <a href="https://www.partsltd.co.uk/retention-schedule" alt="retention schedule" aria-label="retention schedule">https://www.partsltd.co.uk/retention-schedule</a></p>
<h5>Who we share information with</h5>
<p>Data processors</p>
<p><strong>Zume</strong></p>
<p>This data processor does the following activities for us: They host our web and email server, including emails with personal data in body and database storage for database that holds user data.</p>
<h5>Sharing information outside the UK</h5>
<p>Where necessary, we may transfer personal information outside of the UK. When doing so, we comply with the UK GDPR, making sure appropriate safeguards are in place. Please contact us for more information.</p>
<p>Where necessary, our data processors may share personal information outside of the UK. When doing so, they comply with the UK GDPR, making sure appropriate safeguards are in place. Please contact us for more information.</p>
<h5>Your data protection rights</h5>
<p>Under data protection law, you have rights including:</p>
<ul>
<li>Your right of access - You have the right to ask us for copies of your personal data.</li>
<li>Your right to rectification - You have the right to ask us to rectify personal data you think is inaccurate. You also have the right to ask us to complete information you think is incomplete.</li>
<li>Your right to erasure - You have the right to ask us to erase your personal data in certain circumstances.</li>
<li>Your right to restriction of processing - You have the right to ask us to restrict the processing of your personal data in certain circumstances.</li>
<li>Your right to object to processing - You have the right to object to the processing of your personal data in certain circumstances.</li>
<li>Your right to data portability - You have the right to ask that we transfer the personal data you gave us to another organisation, or to you, in certain circumstances.</li>
<li>Your right to withdraw consent When we use consent as our lawful basis you have the right to withdraw your consent.</li>
</ul>
<p>You dont usually need to pay a fee to exercise your rights. If you make a request, we have one calendar month to respond to you.</p>
<p>To make a data protection rights request, please contact us using the contact details at the top of this privacy notice.</p>
<h5>How to complain</h5>
<p>If you have any concerns about our use of your personal data, you can make a complaint to us using the contact details at the top of this privacy notice.</p>
<p>If you remain unhappy with how weve used your data after raising a complaint with us, you can also complain to the ICO.</p>
<h5>The ICOs address:</h5>
<p>Information Commissioners Office</p>
<p>Wycliffe House</p>
<p>Water Lane</p>
<p>Wilmslow</p>
<p>Cheshire</p>
<p>SK9 5AF</p>
<p>Helpline number: 0303 123 1113</p>
<p>Website: <a href="https://www.ico.org.uk/make-a-complaint" alt="ICO make a complaint" aria-label="ICO make a complaint">https://www.ico.org.uk/make-a-complaint</a></p>
<p>Last updated</p>
<p>1 May 2024</p>
</div>
<!-- Include JavaScript -->
<script src="{{ url_for('static', filename='js/accessibility_statement.js') }}"></script>
<script>
var hashPageCurrent = "{{ model.HASH_PAGE_ACCESSIBILITY_STATEMENT }}";
$(document).ready(function() {
hookupPageAccessibilityStatement();
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

4
templates/_shared.html Normal file
View File

@@ -0,0 +1,4 @@
{% if block_id == 'button_get_in_touch' %}
<button class="button-contact" alt="Get in touch" aria-label="Get in touch">Get in touch</button>
{% endif %}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en-GB">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8"/> <meta charset="utf-8"/>
@@ -61,13 +61,13 @@
<div class="topnav"> <div class="topnav">
<div class="{{ model.FLAG_CONTAINER }}" style="width: 18vw; min-width: 18vw; max-width: 20vw;"> <div class="{{ model.FLAG_CONTAINER }}" style="width: 18vw; min-width: 18vw; max-width: 20vw;">
<img class="header-logo" src="{{ url_for('static', filename='images/Logo.png') }}" alt="PARTS logo" tabindex="0"> <img class="header-logo" src="{{ url_for('static', filename='images/Logo.png') }}" alt="{{ model.NAME_COMPANY }} logo" aria-label="{{ model.NAME_COMPANY }} logo" tabindex="0">
</div> </div>
<div class="{{ model.FLAG_CONTAINER }}" style="width: 75vw; min-width: 65vw; max-width: 80vw;"> <div class="{{ model.FLAG_CONTAINER }}" style="width: 75vw; min-width: 65vw; max-width: 80vw;">
<h4 class="company-name">Precision And Research Technology Systems</h4> <h4 class="company-name">Precision And Research Technology Systems</h4>
</div> </div>
<div class="{{ model.FLAG_CONTAINER }}" style="width: 7vw; min-width: 7vw; max-width: 15vw; justify-content: flex-end; "> <!-- padding-left: 25%; --> <div class="{{ model.FLAG_CONTAINER }}" style="width: 7vw; min-width: 7vw; max-width: 15vw; justify-content: flex-end; "> <!-- padding-left: 25%; -->
<button id="{{ model.ID_BUTTON_HAMBURGER }}" tabindex="1">=</button> <button id="{{ model.ID_BUTTON_HAMBURGER }}" tabindex="1" alt="Hamburger menu button" aria-label="Hamburger menu button">=</button>
</div> </div>
</div> </div>
@@ -140,7 +140,7 @@
<div class="footer"> <div class="footer">
<h4 style="padding-top: 1vh;">Copyright &copy; Precision And Research Technology Systems Limited. All rights reserved.</h4> <h4 style="padding-top: 1vh;">Copyright &copy; {{ model.NAME_COMPANY }}. <a href="{{ url_for('license') }}" alt="License" aria-label="License">All rights reserved.</a></h4>
<h5>Company number 13587499</h5> <h5>Company number 13587499</h5>
</div> </div>
@@ -162,3 +162,4 @@
hookupShared(); hookupShared();
}); });
</script> </script>