Updated line endings to be in line with UNIX style

This commit is contained in:
Luke Else 2024-01-28 11:59:04 +00:00
parent 3eea1d946a
commit 5a20a8d7c0
46 changed files with 2536 additions and 2536 deletions

578
.gitignore vendored
View File

@ -1,289 +1,289 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,flask,web # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,flask,web
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,flask,web # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,flask,web
### SQLite 3 Database ### ### SQLite 3 Database ###
data/ data/
static/assets/img/products/ static/assets/img/products/
### CICD Registration files ### ### CICD Registration files ###
cicd/runner-data cicd/runner-data
### Flask ### ### Flask ###
instance/* instance/*
!instance/.gitignore !instance/.gitignore
.webassets-cache .webassets-cache
.env .env
### Flask.Python Stack ### ### Flask.Python Stack ###
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock #pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
.pdm.toml .pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
### Python ### ### Python ###
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
# C extensions # C extensions
# Distribution / packaging # Distribution / packaging
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs # Installer logs
# Unit test / coverage reports # Unit test / coverage reports
# Translations # Translations
# Django stuff: # Django stuff:
# Flask stuff: # Flask stuff:
# Scrapy stuff: # Scrapy stuff:
# Sphinx documentation # Sphinx documentation
# PyBuilder # PyBuilder
# Jupyter Notebook # Jupyter Notebook
# IPython # IPython
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff # Celery stuff
# SageMath parsed files # SageMath parsed files
# Environments # Environments
# Spyder project settings # Spyder project settings
# Rope project settings # Rope project settings
# mkdocs documentation # mkdocs documentation
# mypy # mypy
# Pyre type checker # Pyre type checker
# pytype static type analyzer # pytype static type analyzer
# Cython debug symbols # Cython debug symbols
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ### ### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml poetry.toml
# ruff # ruff
.ruff_cache/ .ruff_cache/
# LSP config files # LSP config files
pyrightconfig.json pyrightconfig.json
### VisualStudioCode ### ### VisualStudioCode ###
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
!.vscode/*.code-snippets !.vscode/*.code-snippets
# Local History for Visual Studio Code # Local History for Visual Studio Code
.history/ .history/
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
### VisualStudioCode Patch ### ### VisualStudioCode Patch ###
# Ignore all local history of files # Ignore all local history of files
.history .history
.ionide .ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,flask,web # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,flask,web

View File

@ -1,11 +1,11 @@
variables: variables:
ENVIRON: "test" ENVIRON: "test"
pytest: pytest:
stage: test stage: test
script: script:
- cd /builds/u5500327/wmgzon - cd /builds/u5500327/wmgzon
# - python -m venv .venv # - python -m venv .venv
# - source /bin/activate # - source /bin/activate
- pip install -r requirements.txt - pip install -r requirements.txt
- pytest --disable-warnings - pytest --disable-warnings

View File

@ -1,43 +1,43 @@
--- ---
runme: runme:
id: 01HK0BF4BTBSKR9VWAP1KGD2S7 id: 01HK0BF4BTBSKR9VWAP1KGD2S7
version: v2.0 version: v2.0
--- ---
# WMGZON # WMGZON
`Flask web application serving WMGZON and its relevant backend services.` `Flask web application serving WMGZON and its relevant backend services.`
## Initialisation ## Initialisation
To start you need to create a virtual environment and load in the required dependencies for the project To start you need to create a virtual environment and load in the required dependencies for the project
```sh {"closeTerminalOnSuccess":"false","id":"01HK0BJCK9BR05J127F1X0RZP9"} ```sh {"closeTerminalOnSuccess":"false","id":"01HK0BJCK9BR05J127F1X0RZP9"}
python -m venv .venv python -m venv .venv
./.venv/Scripts/Activate.ps1 ./.venv/Scripts/Activate.ps1
pip install -r requirements.txt pip install -r requirements.txt
``` ```
## Testing ## Testing
To run the full suite of unit tests for the webapp simply run the following command in the venv To run the full suite of unit tests for the webapp simply run the following command in the venv
```sh ```sh
pytest pytest
``` ```
## Running ## Running
### Pre-Requisites ### Pre-Requisites
- Docker daemon is installed and running. - Docker daemon is installed and running.
- Docker compose is installed. - Docker compose is installed.
### Instructions ### Instructions
In order to run the web app, simply use the command In order to run the web app, simply use the command
```sh {"id":"01HKD0VRADDYQ92W22JN1FHA03"} ```sh {"id":"01HKD0VRADDYQ92W22JN1FHA03"}
docker-compose up -d docker-compose up -d
``` ```
to run the container in a detatched mode. to run the container in a detatched mode.

54
app.py
View File

@ -1,27 +1,27 @@
from flask import Flask from flask import Flask
from os import environ from os import environ
from controllers.web.endpoints import blueprint from controllers.web.endpoints import blueprint
''' '''
Main entrypoint for Flask application. Main entrypoint for Flask application.
Initialises any components that are needed at runtime such as the Initialises any components that are needed at runtime such as the
Database manager... Database manager...
''' '''
app = Flask(__name__) app = Flask(__name__)
# Set app secret key to sign session cookies # Set app secret key to sign session cookies
secret_key = environ.get("APPSECRET") secret_key = environ.get("APPSECRET")
if secret_key is None: if secret_key is None:
# NO Secret Key set! # NO Secret Key set!
print("No app secret set, please set one before deploying in production") print("No app secret set, please set one before deploying in production")
app.secret_key = "DEFAULTKEY" app.secret_key = "DEFAULTKEY"
else: else:
app.secret_key = secret_key app.secret_key = secret_key
# Register a blueprint # Register a blueprint
app.register_blueprint(blueprint) app.register_blueprint(blueprint)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=8080) app.run(debug=True, host="0.0.0.0", port=8080)

View File

@ -1,8 +1,8 @@
version: '3.8' version: '3.8'
services: services:
gitlab-runner: gitlab-runner:
image: gitlab/gitlab-runner:latest image: gitlab/gitlab-runner:latest
volumes: volumes:
- ./runner-data:/etc/gitlab-runner - ./runner-data:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped restart: unless-stopped

View File

@ -1,63 +1,63 @@
from .database import DatabaseController from .database import DatabaseController
from models.category import Category from models.category import Category
class CategoryController(DatabaseController): class CategoryController(DatabaseController):
FIELDS = ['id', 'name'] FIELDS = ['id', 'name']
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def create(self, category: Category): def create(self, category: Category):
params = [ params = [
category.name, category.name,
] ]
self._conn.execute( self._conn.execute(
"INSERT INTO Categories (name) VALUES (?)", "INSERT INTO Categories (name) VALUES (?)",
params params
) )
self._conn.commit() self._conn.commit()
def read(self, id: int = 0) -> Category | None: def read(self, id: int = 0) -> Category | None:
params = [ params = [
id id
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Categories WHERE id = ?", "SELECT * FROM Categories WHERE id = ?",
params params
) )
row = cursor.fetchone() row = cursor.fetchone()
if row is None: if row is None:
return None return None
params = dict(zip(self.FIELDS, row)) params = dict(zip(self.FIELDS, row))
obj = self.new_instance(Category, params) obj = self.new_instance(Category, params)
return obj return obj
def read_all(self) -> list[Category] | None: def read_all(self) -> list[Category] | None:
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Categories", "SELECT * FROM Categories",
) )
rows = cursor.fetchall() rows = cursor.fetchall()
if rows is None: if rows is None:
return None return None
categories = list() categories = list()
for category in rows: for category in rows:
params = dict(zip(self.FIELDS, category)) params = dict(zip(self.FIELDS, category))
obj = self.new_instance(Category, params) obj = self.new_instance(Category, params)
categories.append(obj) categories.append(obj)
return categories return categories
def update(self): def update(self):
print("Doing work") print("Doing work")
def delete(self): def delete(self):
print("Doing work") print("Doing work")

View File

@ -1,81 +1,81 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Mapping, Any from typing import Mapping, Any
import sqlite3 import sqlite3
import os import os
class DatabaseController(ABC): class DatabaseController(ABC):
""" Abstract Base Class to handle database access for each component """ Abstract Base Class to handle database access for each component
in the web app in the web app
""" """
__data_dir = "./data/" __data_dir = "./data/"
__db_name = "wmgzon.db" __db_name = "wmgzon.db"
# Use test file if necessary # Use test file if necessary
if os.environ.get("ENVIRON") == "test": if os.environ.get("ENVIRON") == "test":
__db_name = "test_" + __db_name __db_name = "test_" + __db_name
__sqlitefile = __data_dir + __db_name __sqlitefile = __data_dir + __db_name
def __init__(self): def __init__(self):
""" Initialises the object and creates a connection to the local """ Initialises the object and creates a connection to the local
DB on the server DB on the server
""" """
self._conn = None self._conn = None
try: try:
# Creates a connection and specifies a flag to parse all types # Creates a connection and specifies a flag to parse all types
# back down into Python declared types e.g. date & time # back down into Python declared types e.g. date & time
self._conn = sqlite3.connect( self._conn = sqlite3.connect(
self.__sqlitefile, detect_types=sqlite3.PARSE_DECLTYPES) self.__sqlitefile, detect_types=sqlite3.PARSE_DECLTYPES)
except sqlite3.Error as e: except sqlite3.Error as e:
# Close the connection if still open # Close the connection if still open
if self._conn: if self._conn:
self._conn.close() self._conn.close()
print(e) print(e)
def __del__(self): def __del__(self):
""" Object Destructor which kills the connection to the database """ """ Object Destructor which kills the connection to the database """
if self._conn is not None: if self._conn is not None:
self._conn.close() self._conn.close()
def new_instance(self, of: type, with_fields: Mapping[str, Any]): def new_instance(self, of: type, with_fields: Mapping[str, Any]):
""" Takes a dictionary of fields and returns the object """ Takes a dictionary of fields and returns the object
with those fields populated with those fields populated
""" """
obj = of.__new__(of) obj = of.__new__(of)
for attr, value in with_fields.items(): for attr, value in with_fields.items():
setattr(obj, attr, value) setattr(obj, attr, value)
return obj return obj
""" """
Set of CRUD methods to allow for Data manipulation on the backend Set of CRUD methods to allow for Data manipulation on the backend
""" """
@abstractmethod @abstractmethod
def create(self): def create(self):
""" Abstract method used to create a new record of a given """ Abstract method used to create a new record of a given
type within the database type within the database
""" """
pass pass
@abstractmethod @abstractmethod
def read(self): def read(self):
""" Abstract method used to read a record of a given """ Abstract method used to read a record of a given
type from the database type from the database
""" """
pass pass
@abstractmethod @abstractmethod
def update(self): def update(self):
""" Abstract method used to update a record of a given """ Abstract method used to update a record of a given
type within the database type within the database
""" """
pass pass
@abstractmethod @abstractmethod
def delete(self): def delete(self):
""" Abstract method used to delete record of a given """ Abstract method used to delete record of a given
type from the database type from the database
""" """
pass pass

View File

@ -1,160 +1,160 @@
from .database import DatabaseController from .database import DatabaseController
from models.products.product import Product from models.products.product import Product
class ProductController(DatabaseController): class ProductController(DatabaseController):
FIELDS = ['id', 'name', 'image', 'description', 'cost', FIELDS = ['id', 'name', 'image', 'description', 'cost',
'sellerID', 'category', 'postedDate', 'quantityAvailable'] 'sellerID', 'category', 'postedDate', 'quantityAvailable']
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def create(self, product: Product): def create(self, product: Product):
params = [ params = [
product.name, product.name,
product.image, product.image,
product.description, product.description,
product.cost, product.cost,
product.category, product.category,
product.sellerID, product.sellerID,
product.postedDate, product.postedDate,
product.quantityAvailable product.quantityAvailable
] ]
self._conn.execute( self._conn.execute(
""" """
INSERT INTO Products INSERT INTO Products
(name, image, description, cost, categoryID, (name, image, description, cost, categoryID,
sellerID, postedDate, quantityAvailable) sellerID, postedDate, quantityAvailable)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
params params
) )
self._conn.commit() self._conn.commit()
def read(self, name: str = "") -> list[Product] | None: def read(self, name: str = "") -> list[Product] | None:
params = [ params = [
"%" + name + "%" "%" + name + "%"
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Products WHERE name like ?", "SELECT * FROM Products WHERE name like ?",
params params
) )
rows = cursor.fetchmany() rows = cursor.fetchmany()
if rows is None: if rows is None:
return None return None
products = list() products = list()
# Create an object for each row # Create an object for each row
for product in rows: for product in rows:
params = dict(zip(self.FIELDS, product)) params = dict(zip(self.FIELDS, product))
obj = self.new_instance(Product, params) obj = self.new_instance(Product, params)
products.append(obj) products.append(obj)
return products return products
def read_id(self, id: int) -> Product | None: def read_id(self, id: int) -> Product | None:
params = [ params = [
id id
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Products WHERE id == ?", "SELECT * FROM Products WHERE id == ?",
params params
) )
row = cursor.fetchone() row = cursor.fetchone()
if row is None: if row is None:
return None return None
# Create an object with the row # Create an object with the row
params = dict(zip(self.FIELDS, row)) params = dict(zip(self.FIELDS, row))
obj = self.new_instance(Product, params) obj = self.new_instance(Product, params)
return obj return obj
def read_all(self, category: str = "", def read_all(self, category: str = "",
search_term: str = "") -> list[Product] | None: search_term: str = "") -> list[Product] | None:
params = [ params = [
"%" + category + "%", "%" + category + "%",
"%" + search_term + "%" "%" + search_term + "%"
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"""SELECT * FROM Products """SELECT * FROM Products
INNER JOIN Categories ON Products.categoryID = Categories.id INNER JOIN Categories ON Products.categoryID = Categories.id
WHERE Categories.name LIKE ? WHERE Categories.name LIKE ?
AND Products.name LIKE ? AND Products.name LIKE ?
""", """,
params params
) )
rows = cursor.fetchall() rows = cursor.fetchall()
if len(rows) == 0: if len(rows) == 0:
return None return None
products = list() products = list()
# Create an object for each row # Create an object for each row
for product in rows: for product in rows:
params = dict(zip(self.FIELDS, product)) params = dict(zip(self.FIELDS, product))
obj = self.new_instance(Product, params) obj = self.new_instance(Product, params)
products.append(obj) products.append(obj)
return products return products
def read_user(self, user_id: int) -> list[Product] | None: def read_user(self, user_id: int) -> list[Product] | None:
params = [ params = [
user_id user_id
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"""SELECT * FROM Products """SELECT * FROM Products
WHERE sellerID = ? WHERE sellerID = ?
""", """,
params params
) )
rows = cursor.fetchall() rows = cursor.fetchall()
if len(rows) == 0: if len(rows) == 0:
return None return None
products = list() products = list()
# Create an object for each row # Create an object for each row
for product in rows: for product in rows:
params = dict(zip(self.FIELDS, product)) params = dict(zip(self.FIELDS, product))
obj = self.new_instance(Product, params) obj = self.new_instance(Product, params)
products.append(obj) products.append(obj)
return products return products
def update(self, product: Product): def update(self, product: Product):
params = [ params = [
product.name, product.name,
product.description, product.description,
product.image, product.image,
product.cost, product.cost,
product.quantityAvailable, product.quantityAvailable,
product.category, product.category,
product.id product.id
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"""UPDATE Products """UPDATE Products
SET name = ?, SET name = ?,
description = ?, description = ?,
image = ?, image = ?,
cost = ?, cost = ?,
quantityAvailable = ?, quantityAvailable = ?,
categoryID = ? categoryID = ?
WHERE id = ? WHERE id = ?
""", """,
params params
) )
self._conn.commit() self._conn.commit()
def delete(self): def delete(self):
print("Doing work") print("Doing work")

View File

@ -1,85 +1,85 @@
from .database import DatabaseController from .database import DatabaseController
from models.users.user import User from models.users.user import User
from models.users.customer import Customer from models.users.customer import Customer
from models.users.seller import Seller from models.users.seller import Seller
class UserController(DatabaseController): class UserController(DatabaseController):
FIELDS = ['id', 'username', 'password', 'firstName', FIELDS = ['id', 'username', 'password', 'firstName',
'lastName', 'email', 'phone', 'role'] 'lastName', 'email', 'phone', 'role']
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def create(self, user: User): def create(self, user: User):
params = [ params = [
user.username, user.username,
user.password, user.password,
user.firstName, user.firstName,
user.lastName, user.lastName,
user.email, user.email,
user.phone, user.phone,
user.role user.role
] ]
self._conn.execute( self._conn.execute(
"""INSERT INTO Users """INSERT INTO Users
(username, password, first_name, last_name, email, phone, role) (username, password, first_name, last_name, email, phone, role)
VALUES (?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?)""",
params params
) )
self._conn.commit() self._conn.commit()
def read(self, username: str) -> User | None: def read(self, username: str) -> User | None:
params = [ params = [
username username
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Users WHERE Username = ?", "SELECT * FROM Users WHERE Username = ?",
params params
) )
row = cursor.fetchone() row = cursor.fetchone()
if row is not None: if row is not None:
params = dict(zip(self.FIELDS, row)) params = dict(zip(self.FIELDS, row))
# Is user a seller # Is user a seller
type = Customer type = Customer
if row[7] == "Seller": if row[7] == "Seller":
type = Seller type = Seller
obj = self.new_instance(type, params) obj = self.new_instance(type, params)
return obj return obj
return None return None
def read_id(self, id: int) -> User | None: def read_id(self, id: int) -> User | None:
params = [ params = [
id id
] ]
cursor = self._conn.execute( cursor = self._conn.execute(
"SELECT * FROM Users WHERE id = ?", "SELECT * FROM Users WHERE id = ?",
params params
) )
row = cursor.fetchone() row = cursor.fetchone()
if row is not None: if row is not None:
params = dict(zip(self.FIELDS, row)) params = dict(zip(self.FIELDS, row))
# Is user a seller # Is user a seller
type = Customer type = Customer
if row[7] == "Seller": if row[7] == "Seller":
type = Seller type = Seller
obj = self.new_instance(type, params) obj = self.new_instance(type, params)
return obj return obj
return None return None
def update(self): def update(self):
print("Doing work") print("Doing work")
def delete(self): def delete(self):
print("Doing work") print("Doing work")

View File

@ -1,34 +1,34 @@
from models.users.user import User from models.users.user import User
from controllers.database.user import UserController from controllers.database.user import UserController
from flask import redirect, Blueprint, session from flask import redirect, Blueprint, session
from . import user from . import user
from . import product from . import product
blueprint = Blueprint('main', __name__) blueprint = Blueprint('main', __name__)
blueprint.register_blueprint(user.blueprint) blueprint.register_blueprint(user.blueprint)
blueprint.register_blueprint(product.blueprint) blueprint.register_blueprint(product.blueprint)
# CONTEXTS # # CONTEXTS #
# Function that returns a given user class based on the ID in the session # Function that returns a given user class based on the ID in the session
@blueprint.context_processor @blueprint.context_processor
def get_user() -> dict[User | None]: def get_user() -> dict[User | None]:
# Get the user based on the user ID # Get the user based on the user ID
user_id = session.get('user_id') user_id = session.get('user_id')
user = None user = None
if user_id is not None: if user_id is not None:
db = UserController() db = UserController()
user = db.read_id(user_id) user = db.read_id(user_id)
return dict(user=user) return dict(user=user)
# Function responsible for displaying the main landing page of the site # Function responsible for displaying the main landing page of the site
@blueprint.route('/') @blueprint.route('/')
def index(): def index():
return redirect("/products") return redirect("/products")

View File

@ -1,188 +1,188 @@
""" """
Product related endpoints. Included contexts for principles such as Product related endpoints. Included contexts for principles such as
categories and image processing. categories and image processing.
""" """
from flask import render_template, session, flash, request, redirect, Blueprint from flask import render_template, session, flash, request, redirect, Blueprint
from models.products.product import Product from models.products.product import Product
from controllers.database.product import ProductController from controllers.database.product import ProductController
from controllers.database.category import CategoryController from controllers.database.category import CategoryController
from controllers.database.user import UserController from controllers.database.user import UserController
from datetime import datetime from datetime import datetime
from utils.file_utils import allowed_file, save_image, remove_file from utils.file_utils import allowed_file, save_image, remove_file
from utils.user_utils import is_role from utils.user_utils import is_role
import pathlib import pathlib
import os import os
blueprint = Blueprint("products", __name__, url_prefix="/products") blueprint = Blueprint("products", __name__, url_prefix="/products")
@blueprint.context_processor @blueprint.context_processor
def category_list(): def category_list():
""" Places a list of all categories in the products context """ """ Places a list of all categories in the products context """
database = CategoryController() database = CategoryController()
categories = database.read_all() categories = database.read_all()
return dict(categories=categories) return dict(categories=categories)
@blueprint.route('/') @blueprint.route('/')
def index(): def index():
""" The front product page """ """ The front product page """
# Returning an empty category acts the same # Returning an empty category acts the same
# as a generic home page # as a generic home page
return category("") return category("")
@blueprint.route('/<string:category>') @blueprint.route('/<string:category>')
def category(category: str): def category(category: str):
""" Loads a given categories page """ """ Loads a given categories page """
database = ProductController() database = ProductController()
# Check to see if there is a custome search term # Check to see if there is a custome search term
search_term = request.args.get("search", type=str) search_term = request.args.get("search", type=str)
if search_term is not None: if search_term is not None:
products = database.read_all(category, search_term) products = database.read_all(category, search_term)
else: else:
products = database.read_all(category) products = database.read_all(category)
# No Products visible # No Products visible
if products is None: if products is None:
flash( flash(
f"No Products available. Try expanding your search criteria.", f"No Products available. Try expanding your search criteria.",
"warning" "warning"
) )
return render_template( return render_template(
'index.html', 'index.html',
content="content.html", content="content.html",
products=products, products=products,
category=category category=category
) )
@blueprint.route('/<int:id>') @blueprint.route('/<int:id>')
def id(id: int): def id(id: int):
""" Loads a given product based on ID """ """ Loads a given product based on ID """
db = ProductController() db = ProductController()
product = db.read_id(id) product = db.read_id(id)
# Check that a valid product was returned # Check that a valid product was returned
if product is None: if product is None:
flash(f"No Product available with id {id}", "warning") flash(f"No Product available with id {id}", "warning")
return redirect("/") return redirect("/")
print(product.name) print(product.name)
return render_template( return render_template(
'index.html', 'index.html',
content='product.html', content='product.html',
product=product product=product
) )
@blueprint.route('/add') @blueprint.route('/add')
def display_add_product(): def display_add_product():
""" Launches the page to add a new product to the site """ """ Launches the page to add a new product to the site """
user_id = session.get('user_id') user_id = session.get('user_id')
# User needs to be logged in as a seller to view this page # User needs to be logged in as a seller to view this page
if not is_role("Seller"): if not is_role("Seller"):
flash("You must be logged in as a seller to view this page!", "error") flash("You must be logged in as a seller to view this page!", "error")
return redirect("/") return redirect("/")
return render_template('index.html', content='new_product.html') return render_template('index.html', content='new_product.html')
@blueprint.post('/add') @blueprint.post('/add')
def add_product(): def add_product():
""" Server site processing to handle a request to add a """ Server site processing to handle a request to add a
new product to the site new product to the site
""" """
user_id = session.get('user_id') user_id = session.get('user_id')
# User needs to be logged in as a seller to view this page # User needs to be logged in as a seller to view this page
if not is_role("Seller"): if not is_role("Seller"):
flash("You must be logged in as a seller to view this page!", "error") flash("You must be logged in as a seller to view this page!", "error")
return redirect("/") return redirect("/")
file = request.files.get('image') file = request.files.get('image')
image_filename = save_image(file) image_filename = save_image(file)
product = Product( product = Product(
request.form.get('name'), request.form.get('name'),
image_filename if image_filename is not None else "", image_filename if image_filename is not None else "",
request.form.get('description'), request.form.get('description'),
request.form.get('cost'), request.form.get('cost'),
request.form.get('category'), request.form.get('category'),
user_id, user_id,
datetime.now(), datetime.now(),
request.form.get('quantity') request.form.get('quantity')
) )
db = ProductController() db = ProductController()
db.create(product) db.create(product)
return redirect('/products/ownproducts') return redirect('/products/ownproducts')
@blueprint.post('/update/<int:id>') @blueprint.post('/update/<int:id>')
def update_product(id: int): def update_product(id: int):
""" Processes a request to update a product in place on the site """ """ Processes a request to update a product in place on the site """
# Ensure that the product belongs to the current user # Ensure that the product belongs to the current user
user_id = session.get('user_id') user_id = session.get('user_id')
# User needs to be logged in as a seller to view this page # User needs to be logged in as a seller to view this page
if not is_role("Seller"): if not is_role("Seller"):
flash("You must be logged in as a seller to view this page!", "error") flash("You must be logged in as a seller to view this page!", "error")
return redirect("/") return redirect("/")
db = ProductController() db = ProductController()
product = db.read_id(id) product = db.read_id(id)
if product.sellerID != user_id: if product.sellerID != user_id:
flash("This product does not belong to you!", "error") flash("This product does not belong to you!", "error")
return redirect("/ownproducts") return redirect("/ownproducts")
# Save new image file # Save new image file
file = request.files.get('image') file = request.files.get('image')
new_image = save_image(file) new_image = save_image(file)
if new_image is not None: if new_image is not None:
remove_file(os.path.join(os.environ.get('FILESTORE'), product.image)) remove_file(os.path.join(os.environ.get('FILESTORE'), product.image))
product.image = new_image product.image = new_image
# Update product details # Update product details
product.name = request.form.get('name') product.name = request.form.get('name')
product.description = request.form.get('description') product.description = request.form.get('description')
product.category = request.form.get('category') product.category = request.form.get('category')
product.cost = request.form.get('cost') product.cost = request.form.get('cost')
product.quantityAvailable = request.form.get('quantity') product.quantityAvailable = request.form.get('quantity')
db.update(product) db.update(product)
flash("Product successfully updated", 'notice') flash("Product successfully updated", 'notice')
return redirect(f"/products/{product.id}") return redirect(f"/products/{product.id}")
@blueprint.route('/ownproducts') @blueprint.route('/ownproducts')
def display_own_products(): def display_own_products():
""" Display products owned by the currently logged in seller """ """ Display products owned by the currently logged in seller """
user_id = session.get('user_id') user_id = session.get('user_id')
# User must be logged in as seller to view page # User must be logged in as seller to view page
if not is_role("Seller"): if not is_role("Seller"):
flash("You must be logged in as a seller to view this page!", "error") flash("You must be logged in as a seller to view this page!", "error")
return redirect("/") return redirect("/")
db = ProductController() db = ProductController()
products = db.read_user(user_id) products = db.read_user(user_id)
if products is None: if products is None:
flash("You don't currently have any products for sale.", "info") flash("You don't currently have any products for sale.", "info")
return render_template( return render_template(
'index.html', 'index.html',
content='content.html', content='content.html',
products=products products=products
) )

View File

@ -1,99 +1,99 @@
""" The user controller to manage all of the user related endpoints """ The user controller to manage all of the user related endpoints
in the web app in the web app
""" """
from flask import Blueprint from flask import Blueprint
from flask import render_template, redirect, request, session, flash from flask import render_template, redirect, request, session, flash
from controllers.database.user import UserController from controllers.database.user import UserController
from models.users.user import User from models.users.user import User
from models.users.customer import Customer from models.users.customer import Customer
from models.users.seller import Seller from models.users.seller import Seller
from hashlib import sha512 from hashlib import sha512
# Blueprint to append user endpoints to # Blueprint to append user endpoints to
blueprint = Blueprint("users", __name__) blueprint = Blueprint("users", __name__)
# LOGIN FUNCTIONALITY # LOGIN FUNCTIONALITY
@blueprint.route('/login') @blueprint.route('/login')
def display_login(): def display_login():
""" Function responsible for delivering the Login page for the site """ """ Function responsible for delivering the Login page for the site """
return render_template('index.html', content="login.html") return render_template('index.html', content="login.html")
@blueprint.post('/login') @blueprint.post('/login')
def login(): def login():
""" Function to handle the backend processing of a login request """ """ Function to handle the backend processing of a login request """
database = UserController() database = UserController()
user = database.read(request.form['username']) user = database.read(request.form['username'])
error = None error = None
# No user found # No user found
if user is None: if user is None:
error = "No user found with the username " + request.form['username'] error = "No user found with the username " + request.form['username']
flash(error, 'warning') flash(error, 'warning')
return redirect("/login") return redirect("/login")
# Incorrect Password # Incorrect Password
if sha512(request.form['password'].encode()).hexdigest() != user.password: if sha512(request.form['password'].encode()).hexdigest() != user.password:
error = "Incorrect Password" error = "Incorrect Password"
flash(error, 'warning') flash(error, 'warning')
return redirect("/login") return redirect("/login")
session['user_id'] = user.id session['user_id'] = user.id
return redirect("/") return redirect("/")
# SIGNUP FUNCTIONALITY # SIGNUP FUNCTIONALITY
@blueprint.route('/signup') @blueprint.route('/signup')
def display_signup(): def display_signup():
""" Function responsible for delivering the Signup page for the site """ """ Function responsible for delivering the Signup page for the site """
return render_template('index.html', content="signup.html") return render_template('index.html', content="signup.html")
@blueprint.post('/signup') @blueprint.post('/signup')
def signup(): def signup():
""" Function to handle the backend processing of a signup request """ """ Function to handle the backend processing of a signup request """
database = UserController() database = UserController()
# User already exists # User already exists
if database.read(request.form['username']) is not None: if database.read(request.form['username']) is not None:
error = "User, " + request.form['username'] + " already exists" error = "User, " + request.form['username'] + " already exists"
flash(error, 'warning') flash(error, 'warning')
return redirect("/signup") return redirect("/signup")
# Signup as Seller or Customer # Signup as Seller or Customer
if request.form.get('seller'): if request.form.get('seller'):
user = Seller( user = Seller(
request.form['username'], request.form['username'],
# Hashed as soon as it is recieved on the backend # Hashed as soon as it is recieved on the backend
sha512(request.form['password'].encode()).hexdigest(), sha512(request.form['password'].encode()).hexdigest(),
request.form['firstname'], request.form['firstname'],
request.form['lastname'], request.form['lastname'],
request.form['email'], request.form['email'],
"123" "123"
) )
else: else:
user = Customer( user = Customer(
request.form['username'], request.form['username'],
# Hashed as soon as it is recieved on the backend # Hashed as soon as it is recieved on the backend
sha512(request.form['password'].encode()).hexdigest(), sha512(request.form['password'].encode()).hexdigest(),
request.form['firstname'], request.form['firstname'],
request.form['lastname'], request.form['lastname'],
request.form['email'], request.form['email'],
"123" "123"
) )
database.create(user) database.create(user)
# Code 307 Preserves the original request (POST) # Code 307 Preserves the original request (POST)
return redirect("/login", code=307) return redirect("/login", code=307)
# SIGN OUT FUNCTIONALITY # SIGN OUT FUNCTIONALITY
@blueprint.route('/logout') @blueprint.route('/logout')
def logout(): def logout():
""" Function responsible for handling logouts from the site """ """ Function responsible for handling logouts from the site """
# Clear the current user from the session if they are logged in # Clear the current user from the session if they are logged in
session.pop('user_id', None) session.pop('user_id', None)
return redirect("/") return redirect("/")

View File

@ -1,18 +1,18 @@
version: '3.8' version: '3.8'
services: services:
wmgzon: wmgzon:
container_name: "wmgzon" container_name: "wmgzon"
build: . build: .
environment: environment:
- APPSECRET=test - APPSECRET=test
- ENVIRON=test - ENVIRON=test
#- ENVIRON=prod #- ENVIRON=prod
- FILESTORE=static/assets/img/products/ - FILESTORE=static/assets/img/products/
tty: true tty: true
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- .:/app - .:/app
restart: unless-stopped restart: unless-stopped

View File

@ -1,7 +1,7 @@
FROM python:latest FROM python:latest
COPY ./requirements.txt /app/requirements.txt COPY ./requirements.txt /app/requirements.txt
WORKDIR /app WORKDIR /app
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
COPY . /app COPY . /app
RUN chmod +x scripts/run.bash RUN chmod +x scripts/run.bash
CMD ["bash", "scripts/run.bash"] CMD ["bash", "scripts/run.bash"]

View File

@ -1,8 +1,8 @@
class Category: class Category:
''' '''
Constructor for a category object Constructor for a category object
''' '''
def __init__(self): def __init__(self):
self.id = 0 self.id = 0
self.name = "" self.name = ""

View File

@ -1,15 +1,15 @@
from datetime import datetime from datetime import datetime
class Order: class Order:
''' '''
Constructor for an order object Constructor for an order object
''' '''
def __init__(self): def __init__(self):
self.id = 0 self.id = 0
self.sellerID = 0 self.sellerID = 0
self.customerID = 0 self.customerID = 0
self.products = list() self.products = list()
self.totalCost = 0.0 self.totalCost = 0.0
self.orderDate = datetime.now() self.orderDate = datetime.now()

View File

@ -1,14 +1,14 @@
from product import Product from product import Product
class CarPart(Product): class CarPart(Product):
''' '''
Constructor for a car part Constructor for a car part
Contains additional information that is only relevant for car parts Contains additional information that is only relevant for car parts
''' '''
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.make = "" self.make = ""
self.compatibleVehicles = list() self.compatibleVehicles = list()

View File

@ -1,38 +1,38 @@
from datetime import datetime from datetime import datetime
class Product: class Product:
''' '''
Base class for a product Base class for a product
''' '''
def __init__(self): def __init__(self):
self.id = 0 self.id = 0
self.name = "" self.name = ""
self.image = "/static/assets/wmgzon.png" self.image = "/static/assets/wmgzon.png"
self.description = "" self.description = ""
self.cost = 0.0 self.cost = 0.0
self.category = 0 self.category = 0
self.sellerID = 0 self.sellerID = 0
self.postedDate = datetime.now() self.postedDate = datetime.now()
self.quantityAvailable = 0 self.quantityAvailable = 0
''' '''
Class constructor to instatiate a customer object Class constructor to instatiate a customer object
No additional properties are assigned to the customer No additional properties are assigned to the customer
''' '''
def __init__(self, name: str, image: str, description: str, def __init__(self, name: str, image: str, description: str,
cost: float, category: int, seller_id: int, cost: float, category: int, seller_id: int,
posted_date: datetime, quantity_available: int posted_date: datetime, quantity_available: int
): ):
self.id = 0 self.id = 0
self.name = name self.name = name
self.image = image self.image = image
self.description = description self.description = description
self.cost = cost self.cost = cost
self.category = category self.category = category
self.sellerID = seller_id self.sellerID = seller_id
self.postedDate = posted_date self.postedDate = posted_date
self.quantityAvailable = quantity_available self.quantityAvailable = quantity_available

View File

@ -1,15 +1,15 @@
from .user import User from .user import User
class Admin(User): class Admin(User):
''' '''
Class constructor to instatiate an admin object Class constructor to instatiate an admin object
No additional properties are assigned to the admin No additional properties are assigned to the admin
''' '''
def __init__(self, username: str, password: str, firstname: str, def __init__(self, username: str, password: str, firstname: str,
lastname: str, email: str, phone: str): lastname: str, email: str, phone: str):
super().__init__( super().__init__(
username, password, firstname, lastname, email, phone, "Admin" username, password, firstname, lastname, email, phone, "Admin"
) )

View File

@ -1,15 +1,15 @@
from .user import User from .user import User
class Customer(User): class Customer(User):
''' '''
Class constructor to instatiate a customer object Class constructor to instatiate a customer object
No additional properties are assigned to the customer No additional properties are assigned to the customer
''' '''
def __init__(self, username: str, password: str, firstname: str, def __init__(self, username: str, password: str, firstname: str,
lastname: str, email: str, phone: str): lastname: str, email: str, phone: str):
super().__init__( super().__init__(
username, password, firstname, lastname, email, phone, "Customer" username, password, firstname, lastname, email, phone, "Customer"
) )

View File

@ -1,16 +1,16 @@
from .user import User from .user import User
class Seller(User): class Seller(User):
''' '''
Class constructor to instatiate a customer object Class constructor to instatiate a customer object
No additional properties are assigned to the customer No additional properties are assigned to the customer
''' '''
def __init__(self, username: str, password: str, firstname: str, def __init__(self, username: str, password: str, firstname: str,
lastname: str, email: str, phone: str): lastname: str, email: str, phone: str):
super().__init__( super().__init__(
username, password, firstname, lastname, email, phone, "Seller" username, password, firstname, lastname, email, phone, "Seller"
) )
self.store = "" self.store = ""

View File

@ -1,20 +1,20 @@
from abc import ABC from abc import ABC
class User(ABC): class User(ABC):
""" Functional Class constructor to initialise all properties in """ Functional Class constructor to initialise all properties in
the base object with a value the base object with a value
""" """
def __init__(self, username: str, password: str, def __init__(self, username: str, password: str,
firstname: str, lastname: str, firstname: str, lastname: str,
email: str, phone: str, role: str email: str, phone: str, role: str
): ):
self.id = 0 self.id = 0
self.username = username self.username = username
self.password = password self.password = password
self.firstName = firstname self.firstName = firstname
self.lastName = lastname self.lastName = lastname
self.email = email self.email = email
self.phone = phone self.phone = phone
self.role = role self.role = role

View File

@ -1,26 +1,26 @@
blinker==1.7.0 blinker==1.7.0
click==8.1.7 click==8.1.7
colorama==0.4.6 colorama==0.4.6
distlib==0.3.7 distlib==0.3.7
dnspython==2.4.2 dnspython==2.4.2
docstring-to-markdown==0.13 docstring-to-markdown==0.13
filelock==3.13.1 filelock==3.13.1
Flask==3.0.0 Flask==3.0.0
iniconfig==2.0.0 iniconfig==2.0.0
itsdangerous==2.1.2 itsdangerous==2.1.2
jedi==0.19.1 jedi==0.19.1
Jinja2==3.1.2 Jinja2==3.1.2
MarkupSafe==2.1.3 MarkupSafe==2.1.3
packaging==23.2 packaging==23.2
parso==0.8.3 parso==0.8.3
platformdirs==3.11.0 platformdirs==3.11.0
pluggy==1.3.0 pluggy==1.3.0
pymongo==4.6.0 pymongo==4.6.0
pytest==7.4.3 pytest==7.4.3
python-lsp-jsonrpc==1.1.2 python-lsp-jsonrpc==1.1.2
python-lsp-server==1.9.0 python-lsp-server==1.9.0
ujson==5.8.0 ujson==5.8.0
virtualenv==20.24.6 virtualenv==20.24.6
waitress==2.1.2 waitress==2.1.2
Werkzeug==3.0.1 Werkzeug==3.0.1
pycodestyle==2.11.1 pycodestyle==2.11.1

View File

@ -1,51 +1,51 @@
import sqlite3 import sqlite3
import os import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
sys.path.append(os.getcwd()) sys.path.append(os.getcwd())
from utils.file_utils import create_directory, remove_file from utils.file_utils import create_directory, remove_file
def create_connection(path: str, filename: str): def create_connection(path: str, filename: str):
""" create a database connection to a SQLite database """ """ create a database connection to a SQLite database """
conn = None conn = None
try: try:
# Make the directory for the file to go into # Make the directory for the file to go into
create_directory(path) create_directory(path)
print("Opening Database file and ensuring table integrity") print("Opening Database file and ensuring table integrity")
conn = sqlite3.connect(path + filename) conn = sqlite3.connect(path + filename)
print("Database file open") print("Database file open")
# Execute creation scripts # Execute creation scripts
sql = open("scripts/create_tables.sql", "r") sql = open("scripts/create_tables.sql", "r")
conn.executescript(sql.read()) conn.executescript(sql.read())
print("Table creation complete") print("Table creation complete")
# Populate with test data if we are in Test Mode # Populate with test data if we are in Test Mode
if os.environ.get("ENVIRON") == "test": if os.environ.get("ENVIRON") == "test":
sql = open("scripts/test_data.sql", "r") sql = open("scripts/test_data.sql", "r")
conn.executescript(sql.read()) conn.executescript(sql.read())
except sqlite3.Error as e: except sqlite3.Error as e:
print(e) print(e)
finally: finally:
if conn: if conn:
conn.close() conn.close()
# Ensure a directory is created given a path to it # Ensure a directory is created given a path to it
dir = r"./data/" dir = r"./data/"
db_name = r"wmgzon.db" db_name = r"wmgzon.db"
# Check for test environ # Check for test environ
if os.environ.get("ENVIRON") == "test": if os.environ.get("ENVIRON") == "test":
# Remove the original test database # Remove the original test database
print("TEST ENVIRONMENT ACTIVE") print("TEST ENVIRONMENT ACTIVE")
db_name = "test_" + db_name db_name = "test_" + db_name
remove_file(dir + db_name) remove_file(dir + db_name)
create_connection(dir, db_name) create_connection(dir, db_name)

View File

@ -1,56 +1,56 @@
CREATE TABLE IF NOT EXISTS Users ( CREATE TABLE IF NOT EXISTS Users (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL, password TEXT NOT NULL,
first_name TEXT NOT NULL, first_name TEXT NOT NULL,
last_name TEXT NOT NULL, last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
phone TEXT NOT NULL, phone TEXT NOT NULL,
role TEXT NOT NULL role TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS Categories ( CREATE TABLE IF NOT EXISTS Categories (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
); );
INSERT INTO Categories (name) VALUES ("Car Parts"); INSERT INTO Categories (name) VALUES ("Car Parts");
INSERT INTO Categories (name) VALUES ("Animals"); INSERT INTO Categories (name) VALUES ("Animals");
INSERT INTO Categories (name) VALUES ("Sports"); INSERT INTO Categories (name) VALUES ("Sports");
INSERT INTO Categories (name) VALUES ("Books"); INSERT INTO Categories (name) VALUES ("Books");
INSERT INTO Categories (name) VALUES ("Phones"); INSERT INTO Categories (name) VALUES ("Phones");
INSERT INTO Categories (name) VALUES ("Music"); INSERT INTO Categories (name) VALUES ("Music");
CREATE TABLE IF NOT EXISTS Products ( CREATE TABLE IF NOT EXISTS Products (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
image TEXT NOT NULL, image TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
cost DECIMAL NOT NULL, cost DECIMAL NOT NULL,
sellerID INTEGER NOT NULL sellerID INTEGER NOT NULL
REFERENCES Users (id) REFERENCES Users (id)
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
categoryID INTEGER NOT NULL categoryID INTEGER NOT NULL
REFERENCES Categories (id) REFERENCES Categories (id)
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
postedDate TIMESTAMP, postedDate TIMESTAMP,
quantityAvailable INTEGER DEFAULT 0 quantityAvailable INTEGER DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS Orders ( CREATE TABLE IF NOT EXISTS Orders (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
sellerID TEXT NOT NULL sellerID TEXT NOT NULL
REFERENCES Users (id) REFERENCES Users (id)
ON DELETE NO ACTION ON DELETE NO ACTION
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
total DECIMAL NOT NULL, total DECIMAL NOT NULL,
buyerID INTEGER NOT NULL buyerID INTEGER NOT NULL
REFERENCES Users (id) REFERENCES Users (id)
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE NO ACTION, ON UPDATE NO ACTION,
orderDate DATE NOT NULL orderDate DATE NOT NULL
); );

View File

@ -1,37 +1,37 @@
INSERT INTO Users (first_name, last_name, username, email, phone, password, role) VALUES ("Luke", "Else", "lukejelse04", "test@test.com", "07498 289321", "test213", "Customer"); INSERT INTO Users (first_name, last_name, username, email, phone, password, role) VALUES ("Luke", "Else", "lukejelse04", "test@test.com", "07498 289321", "test213", "Customer");
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 1);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 2);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 3);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 4);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);
INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6); INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "brake-disks.png", "this is a product", 20.99, 1, 6);

View File

@ -1,73 +1,73 @@
.alert { .alert {
position: fixed; position: fixed;
bottom: 3em; bottom: 3em;
left: 50%; left: 50%;
transform: translate(-50%, 50%); transform: translate(-50%, 50%);
z-index: 1; z-index: 1;
width: auto; width: auto;
height: auto; height: auto;
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
line-height: 1.8; line-height: 1.8;
border-radius: 5px; border-radius: 5px;
cursor: hand; cursor: hand;
cursor: pointer; cursor: pointer;
font-family: sans-serif; font-family: sans-serif;
font-weight: 400; font-weight: 400;
} }
.alertCheckbox { .alertCheckbox {
display: none; display: none;
} }
:checked + .alert { :checked + .alert {
display: none; display: none;
} }
.alertText { .alertText {
display: table; display: table;
margin: 0 auto; margin: 0 auto;
padding: 0 5px 0 0; padding: 0 5px 0 0;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
} }
.alertClose { .alertClose {
float: right; float: right;
padding-top: 5px; padding-top: 5px;
font-size: 15px; font-size: 15px;
} }
.clear { .clear {
clear: both; clear: both;
} }
.info { .info {
background-color: #EEE; background-color: #EEE;
border: 1px solid #DDD; border: 1px solid #DDD;
color: #999; color: #999;
} }
.success { .success {
background-color: #EFE; background-color: #EFE;
border: 1px solid #DED; border: 1px solid #DED;
color: #9A9; color: #9A9;
} }
.notice { .notice {
background-color: #EFF; background-color: #EFF;
border: 1px solid #DEE; border: 1px solid #DEE;
color: #9AA; color: #9AA;
} }
.warning { .warning {
background-color: #FDF7DF; background-color: #FDF7DF;
border: 1px solid #FEEC6F; border: 1px solid #FEEC6F;
color: #C9971C; color: #C9971C;
} }
.error { .error {
background-color: #FEE; background-color: #FEE;
border: 1px solid #EDD; border: 1px solid #EDD;
color: #A66; color: #A66;
} }

View File

@ -1,69 +1,69 @@
.filter-pane { .filter-pane {
flex: 0 1 auto; flex: 0 1 auto;
width: 60%; width: 60%;
padding: .5rem 2rem; padding: .5rem 2rem;
background-color: var(--bg-grad-3); background-color: var(--bg-grad-3);
border-radius: 1rem; border-radius: 1rem;
} }
.filter-items { .filter-items {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 2rem; gap: 2rem;
} }
.product-filter { .product-filter {
width: 100%; width: 100%;
padding: 16px 20px; padding: 16px 20px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
background-color: var(--bg-grad-2); background-color: var(--bg-grad-2);
color: var(--fg); color: var(--fg);
font-size: .75rem; font-size: .75rem;
} }
/* Number Plate*/ /* Number Plate*/
.number-plate { .number-plate {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.country-identifier { .country-identifier {
width: auto; width: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
color: #ffffff; color: #ffffff;
font-weight: bold; font-weight: bold;
height: 60px; height: 60px;
justify-content: center; justify-content: center;
} }
.country-identifier img { .country-identifier img {
border-radius: 8px 0 0 8px; border-radius: 8px 0 0 8px;
} }
.vrn { .vrn {
width: 10rem; width: 10rem;
height: 63px; height: 63px;
border: 1px solid #ead809; border: 1px solid #ead809;
background-color: #ead809; background-color: #ead809;
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
} }
.vrn .vrn-text { .vrn .vrn-text {
width: -webkit-fill-available; width: -webkit-fill-available;
height: -webkit-fill-available; height: -webkit-fill-available;
border-radius: 0; border-radius: 0;
border: 0; border: 0;
font-family: "UKNumberPlate", sans-serif; font-family: "UKNumberPlate", sans-serif;
font-size: 180%; font-size: 180%;
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
outline: 0; outline: 0;
background-color: transparent; background-color: transparent;
} }
.vrn .vrn-text:focus { .vrn .vrn-text:focus {
outline: 0; outline: 0;
} }

View File

@ -1,155 +1,155 @@
h2 { h2 {
font-weight:300; font-weight:300;
text-align:center; text-align:center;
} }
#input-form-wrap { #input-form-wrap {
background-color: rgba(255, 255, 255, .15); background-color: rgba(255, 255, 255, .15);
backdrop-filter: blur(200px); backdrop-filter: blur(200px);
width: 35%; width: 35%;
text-align: center; text-align: center;
padding: 1em 0 0 0; padding: 1em 0 0 0;
border-radius: 4px; border-radius: 4px;
box-shadow: 0px 30px 50px 0px rgba(0, 0, 0, 0.2); box-shadow: 0px 30px 50px 0px rgba(0, 0, 0, 0.2);
} }
.input-form { .input-form {
padding: 1em 2em; padding: 1em 2em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-items: center; justify-items: center;
gap: 1em; gap: 1em;
} }
.input-form-row { .input-form-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em 1em; gap: 1em 1em;
} }
.input-form input, .input-form select, .input-form option, .input-form textarea { .input-form input, .input-form select, .input-form option, .input-form textarea {
width: 100%; width: 100%;
padding: 0 0 0 10px; padding: 0 0 0 10px;
margin: 0; margin: 0;
color: var(--fg); color: var(--fg);
border: 1px solid var(--fg); border: 1px solid var(--fg);
box-sizing: border-box; box-sizing: border-box;
outline: none; outline: none;
height: 60px; height: 60px;
line-height: 60px; line-height: 60px;
border-radius: 4px; border-radius: 4px;
font-style: normal; font-style: normal;
font-size: 16px; font-size: 16px;
appearance: none; appearance: none;
position: relative; position: relative;
display: inline-block; display: inline-block;
background: none; background: none;
&:focus { &:focus {
&:invalid { &:invalid {
color: var(--red); color: var(--red);
border-color: var(--red); border-color: var(--red);
} }
} }
&:valid { &:valid {
border-color: var(--green); border-color: var(--green);
} }
} }
.input-form textarea { .input-form textarea {
min-height: 120px; min-height: 120px;
max-width: 100%; max-width: 100%;
} }
.input-form input[type="submit"] { .input-form input[type="submit"] {
border: none; border: none;
display:block; display:block;
background-color: rgba(255, 255, 255, .10); background-color: rgba(255, 255, 255, .10);
color: var(--fg); color: var(--fg);
font-weight: bold; font-weight: bold;
text-transform:uppercase; text-transform:uppercase;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
font-size: 18px; font-size: 18px;
position: relative; position: relative;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
&:hover { &:hover {
background-color: rgba(255, 255, 255, .20); background-color: rgba(255, 255, 255, .20);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
} }
#create-account-wrap { #create-account-wrap {
background-color: rgba(255, 255, 255, .15); background-color: rgba(255, 255, 255, .15);
color:#dfdfdf; color:#dfdfdf;
font-size:14px; font-size:14px;
width:100%; width:100%;
padding:10px 0; padding:10px 0;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
.checkbox { .checkbox {
display: block; display: block;
position: relative; position: relative;
padding-left: 35px; padding-left: 35px;
margin-bottom: 12px; margin-bottom: 12px;
cursor: pointer; cursor: pointer;
font-size: 22px; font-size: 22px;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
/* Hide the browser's default checkbox */ /* Hide the browser's default checkbox */
.checkbox input { .checkbox input {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
height: 0; height: 0;
width: 0; width: 0;
} }
/* Create a custom checkbox */ /* Create a custom checkbox */
.checkmark { .checkmark {
position: absolute; position: absolute;
height: 25px; height: 25px;
width: 25px; width: 25px;
background-color: #808080; background-color: #808080;
} }
/* On mouse-over, add a grey background color */ /* On mouse-over, add a grey background color */
.checkbox:hover input ~ .checkmark { .checkbox:hover input ~ .checkmark {
background-color: #ccc; background-color: #ccc;
} }
/* When the checkbox is checked, add a blue background */ /* When the checkbox is checked, add a blue background */
.checkbox input:checked ~ .checkmark { .checkbox input:checked ~ .checkmark {
background-color: #2196F3; background-color: #2196F3;
} }
/* Create the checkmark/indicator (hidden when not checked) */ /* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after { .checkmark:after {
content: ""; content: "";
position: absolute; position: absolute;
display: none; display: none;
} }
/* Show the checkmark when checked */ /* Show the checkmark when checked */
.checkbox input:checked ~ .checkmark:after { .checkbox input:checked ~ .checkmark:after {
display: block; display: block;
} }
/* Style the checkmark/indicator */ /* Style the checkmark/indicator */
.checkbox .checkmark:after { .checkbox .checkmark:after {
left: 9px; left: 9px;
top: 5px; top: 5px;
width: 5px; width: 5px;
height: 10px; height: 10px;
border: solid white; border: solid white;
border-width: 0 3px 3px 0; border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg); -ms-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
} }

View File

@ -1,163 +1,163 @@
/* Product Information*/ /* Product Information*/
.product-link:after { .product-link:after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 15%; left: 15%;
width: 0%; width: 0%;
border-bottom: 2px solid var(--fg); border-bottom: 2px solid var(--fg);
transition: 0.4s; transition: 0.4s;
} }
.product-link:hover:after { .product-link:hover:after {
width: 70%; width: 70%;
color: var(--hover); color: var(--hover);
} }
.product-link:hover { .product-link:hover {
color: var(--hover); color: var(--hover);
} }
.product-link:active { .product-link:active {
color: var(--header); color: var(--header);
} }
.product-container { .product-container {
width: 95%; width: 95%;
background-color: var(--bg-grad-3); background-color: var(--bg-grad-3);
border-radius: 1rem 1rem 0rem 0rem; border-radius: 1rem 1rem 0rem 0rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
gap: 2rem 1.5rem; gap: 2rem 1.5rem;
padding: 1rem; padding: 1rem;
transition: all 0.2s; transition: all 0.2s;
overflow-y: scroll; overflow-y: scroll;
} }
.product { .product {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex: 4 0 1rem; flex: 4 0 1rem;
max-width: 40%; max-width: 40%;
gap: .5em .5em; gap: .5em .5em;
padding: .5rem 0.2rem; padding: .5rem 0.2rem;
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: .5rem .5rem; border-radius: .5rem .5rem;
border: .1em solid rgba(255, 255, 255, 15%); border: .1em solid rgba(255, 255, 255, 15%);
transition: all 0.2s; transition: all 0.2s;
} }
.product-fs { .product-fs {
height: 80%; height: 80%;
width: 80%; width: 80%;
font-size: 150%; font-size: 150%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
gap: 2em 2em; gap: 2em 2em;
padding: .5rem 1rem 2rem 2rem; padding: .5rem 1rem 2rem 2rem;
/* background: var(--bg-secondary); */ /* background: var(--bg-secondary); */
border-radius: .5rem .5rem; border-radius: .5rem .5rem;
transition: all 0.2s; transition: all 0.2s;
} }
.product-content-container { .product-content-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em 1em; gap: 1em 1em;
padding: 0rem 1rem; padding: 0rem 1rem;
} }
.product-image-preview { .product-image-preview {
max-height: 150px; max-height: 150px;
max-width: 150px; max-width: 150px;
} }
.product-image { .product-image {
max-height: 500px; max-height: 500px;
max-width: 500px; max-width: 500px;
} }
.product-title { .product-title {
font-size: 120%; font-size: 120%;
font-weight: bold; font-weight: bold;
} }
.product-details { .product-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 5% 0; padding: 5% 0;
/* justify-content: center; */ /* justify-content: center; */
/* align-items: center; */ /* align-items: center; */
gap: 1rem 1rem; gap: 1rem 1rem;
} }
.product-description { .product-description {
font-size: 70%; font-size: 70%;
} }
.hide-overflow { .hide-overflow {
inline-size: 200px; inline-size: 200px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.product-acquisition-pane { .product-acquisition-pane {
font-size: 80%; font-size: 80%;
width: 100rem; width: 100rem;
max-width: 20%; max-width: 20%;
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
border: .3em solid rgba(255, 255, 255, 15%); border: .3em solid rgba(255, 255, 255, 15%);
border-radius: .5rem; border-radius: .5rem;
padding: 1rem 1rem; padding: 1rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
gap: 1rem 1rem; gap: 1rem 1rem;
} }
.product-price { .product-price {
font-weight: bold; font-weight: bold;
} }
.product-delivery { .product-delivery {
font-size: 70%; font-size: 70%;
} }
.product-postage { .product-postage {
font-size: 70%; font-size: 70%;
} }
.product-stock { .product-stock {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-weight: bold; font-weight: bold;
justify-content: space-around; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
} }
.product-quantity { .product-quantity {
font-size: 50%; font-size: 50%;
} }
.product-instock { .product-instock {
color: var(--green); color: var(--green);
} }
.product-nostock { .product-nostock {
color: var(--red); color: var(--red);
} }
.product-add-to-cart { .product-add-to-cart {
padding: .25rem .5rem; padding: .25rem .5rem;
margin-top: .25rem; margin-top: .25rem;
margin-right: .5rem; margin-right: .5rem;
font-size: 1rem; font-size: 1rem;
border: none; border: none;
background-color: gray; background-color: gray;
} }

View File

@ -1,174 +1,174 @@
:root { :root {
--bg: #282c34; --bg: #282c34;
--bg-secondary: #474d57; --bg-secondary: #474d57;
--bg-grad-1: #484e58; --bg-grad-1: #484e58;
--bg-grad-2: #4e5560; --bg-grad-2: #4e5560;
--bg-grad-3: #59616d; --bg-grad-3: #59616d;
--bg-grad-4: #606a7b; --bg-grad-4: #606a7b;
--bg-grad-5: #606978; --bg-grad-5: #606978;
--input: #4e5560; --input: #4e5560;
--fg: #ABB2BF; --fg: #ABB2BF;
--header: #E06C75; --header: #E06C75;
--link: #FFF; --link: #FFF;
--hover: #888; --hover: #888;
--green: #98C379; --green: #98C379;
--red: #E06C75; --red: #E06C75;
} }
html { html {
background-color: var(--bg); background-color: var(--bg);
color: var(--fg); color: var(--fg);
font-size: 160%; font-size: 160%;
font-family: 'Courier New', Courier, 'Inter'; font-family: 'Courier New', Courier, 'Inter';
height: 100%; height: 100%;
} }
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
margin: 0; margin: 0;
height: 100%; height: 100%;
} }
.navbar { .navbar {
flex: 0 1 auto; flex: 0 1 auto;
padding: 30px; padding: 30px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
/* Navbar styling*/ /* Navbar styling*/
nav { nav {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 120%; font-size: 120%;
} }
nav form { nav form {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.search-bar { .search-bar {
padding: .25rem; padding: .25rem;
margin-top: .25rem; margin-top: .25rem;
font-size: 1rem; font-size: 1rem;
width: 15rem; width: 15rem;
border: none; border: none;
transition: width 0.4s ease-in-out; transition: width 0.4s ease-in-out;
background-color: var(--fg); background-color: var(--fg);
} }
@media (min-width:1500px) { @media (min-width:1500px) {
.search-bar:focus { .search-bar:focus {
width: 30rem; width: 30rem;
} }
} }
.search-button { .search-button {
padding: .25rem .5rem; padding: .25rem .5rem;
margin-top: .25rem; margin-top: .25rem;
margin-right: .5rem; margin-right: .5rem;
background: #ddd; background: #ddd;
font-size: 1rem; font-size: 1rem;
border: none; border: none;
background-color: orange; background-color: orange;
} }
.sticky { .sticky {
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
top: -2rem; top: -2rem;
} }
#logo { #logo {
width: 8rem; width: 8rem;
height: auto; height: auto;
} }
*::-webkit-scrollbar, *::-webkit-scrollbar,
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
width: 26px; width: 26px;
border-radius: 13px; border-radius: 13px;
background-clip: padding-box; background-clip: padding-box;
border: 10px solid transparent; border: 10px solid transparent;
color: var(--fg); color: var(--fg);
} }
*::-webkit-scrollbar-thumb:hover{ *::-webkit-scrollbar-thumb:hover{
color: var(--link); color: var(--link);
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 0 10px; box-shadow: inset 0 0 0 10px;
} }
@media (max-width:1000px) { @media (max-width:1000px) {
.not-required { .not-required {
display: none; display: none;
} }
} }
a { a {
text-decoration: none; text-decoration: none;
position: relative; position: relative;
color: var(--link); color: var(--link);
white-space: nowrap; white-space: nowrap;
transition: 0.4s; transition: 0.4s;
align-content: center; align-content: center;
} }
a:after { a:after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 15%; left: 15%;
width: 0%; width: 0%;
border-bottom: 2px solid var(--fg); border-bottom: 2px solid var(--fg);
transition: 0.4s; transition: 0.4s;
} }
a:hover:after { a:hover:after {
width: 70%; width: 70%;
color: var(--hover); color: var(--hover);
} }
a:hover { a:hover {
color: var(--hover); color: var(--hover);
} }
a:active { a:active {
color: var(--header); color: var(--header);
} }
.categories { .categories {
background-color: var(--bg); background-color: var(--bg);
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.category { .category {
margin: .5rem 0rem; margin: .5rem 0rem;
padding: 0rem 1rem; padding: 0rem 1rem;
border-left: .125rem solid var(--fg); border-left: .125rem solid var(--fg);
} }
.category:first-child { .category:first-child {
border-left: none; border-left: none;
} }
.container { .container {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 1rem; gap: 1rem;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
height: 1%; height: 1%;
} }

View File

@ -1,22 +1,22 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/carparts.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/carparts.css') }}">
<div class="filter-pane"> <div class="filter-pane">
<form action="" method="get" class="filter-items"> <form action="" method="get" class="filter-items">
<div class="number-plate"> <div class="number-plate">
<span class="country-identifier"> <span class="country-identifier">
<img src="https://mycarneedsa.com/assets/flint/img/flag_europe_gb.png" alt=""> <img src="https://mycarneedsa.com/assets/flint/img/flag_europe_gb.png" alt="">
</span> </span>
<span class="vrn"> <span class="vrn">
<input type="text" class="vrn-text" placeholder="YOUR REG" name="vrn"> <input type="text" class="vrn-text" placeholder="YOUR REG" name="vrn">
</span> </span>
</div> </div>
<select class="product-filter not-required" name="filter"> <select class="product-filter not-required" name="filter">
<option value="relevance">Most Relevant</option> <option value="relevance">Most Relevant</option>
<option value="price-lh">Price: Low -> High</option> <option value="price-lh">Price: Low -> High</option>
<option value="price-hl">Price: High -> Low</option> <option value="price-hl">Price: High -> Low</option>
</select> </select>
<input type="submit" class="search-button" value="Filter"> <input type="submit" class="search-button" value="Filter">
</form> </form>
</div> </div>

View File

@ -1,20 +1,20 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}">
<div class="product-container"> <div class="product-container">
{% if products != None %} {% if products != None %}
{% for product in products %} {% for product in products %}
<a href="/products/{{product.id}}" class="product product-link"> <a href="/products/{{product.id}}" class="product product-link">
<div class="product-title">{{product.name}}</div> <div class="product-title">{{product.name}}</div>
<div class="product-content-container"> <div class="product-content-container">
<img class="product-image-preview" src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks" /> <img class="product-image-preview" src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks" />
<div class="product-details"> <div class="product-details">
<div class="product-price">£{{product.cost}}</div> <div class="product-price">£{{product.cost}}</div>
<div class="product-description hide-overflow ">{{product.description}}</div> <div class="product-description hide-overflow ">{{product.description}}</div>
</div> </div>
</div> </div>
<input type="submit" class="product-add-to-cart" value="Add to Cart" /> <input type="submit" class="product-add-to-cart" value="Add to Cart" />
</a> </a>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,51 +1,51 @@
<div class="navbar"> <div class="navbar">
<nav> <nav>
<a href="/"><img src="{{url_for('static', filename='assets/img/wmgzon.png')}}" id="logo" class="not-required" alt="WMGZON Logo"></a> <a href="/"><img src="{{url_for('static', filename='assets/img/wmgzon.png')}}" id="logo" class="not-required" alt="WMGZON Logo"></a>
<form action="" method="get"> <form action="" method="get">
<input type="text" name="search" placeholder="Find your favourite products" class="search-bar"> <input type="text" name="search" placeholder="Find your favourite products" class="search-bar">
<input type="submit" class="search-button"> <input type="submit" class="search-button">
</form> </form>
{% if user != None: %} {% if user != None: %}
<a href="/logout">Welcome, {{ user.username }}</a> <a href="/logout">Welcome, {{ user.username }}</a>
{% else %} {% else %}
<a href="/login">Login/Signup</a> <a href="/login">Login/Signup</a>
{% endif %} {% endif %}
</nav> </nav>
<centre> <centre>
{% if user != None and user.role == "Seller" %} {% if user != None and user.role == "Seller" %}
<div class="categories"> <div class="categories">
{# List all available seller tools #} {# List all available seller tools #}
<a href="/products/add" class="category">Create Products</a> <a href="/products/add" class="category">Create Products</a>
<a href="/products/ownproducts" class="category">View My Products</a> <a href="/products/ownproducts" class="category">View My Products</a>
<a href="/products/stats" class="category">View Seller Stats</a> <a href="/products/stats" class="category">View Seller Stats</a>
</div> </div>
{% endif %} {% endif %}
<div class="categories"> <div class="categories">
{# List all categories and ensure the selected one is highlighted #} {# List all categories and ensure the selected one is highlighted #}
{% for c in categories %} {% for c in categories %}
{% if category == c.name %} {% if category == c.name %}
<a style="color: cyan" href="/products/{{c.name}}" class="category">{{c.name}}</a> <a style="color: cyan" href="/products/{{c.name}}" class="category">{{c.name}}</a>
{% else %} {% else %}
<a href="/products/{{c.name}}" class="category">{{c.name}}</a> <a href="/products/{{c.name}}" class="category">{{c.name}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
</centre> </centre>
</div> </div>
{% with messages = get_flashed_messages(with_categories=true)%} {% with messages = get_flashed_messages(with_categories=true)%}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
<label> <label>
<input type="checkbox" class="alertCheckbox" autocomplete="off" /> <input type="checkbox" class="alertCheckbox" autocomplete="off" />
<div class="alert {{category}}"> <div class="alert {{category}}">
<span class="alertClose">X</span> <span class="alertClose">X</span>
<span class="alertText">{{message}} <span class="alertText">{{message}}
<br class="clear"/></span> <br class="clear"/></span>
</div> </div>
</label> </label>
{% endfor%} {% endfor%}
{% endif %} {% endif %}
{% endwith %} {% endwith %}

View File

@ -1,28 +1,28 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}" /> <link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}" />
<link rel="stylesheet" href="{{url_for('static', filename='css/alerts.css')}}" /> <link rel="stylesheet" href="{{url_for('static', filename='css/alerts.css')}}" />
<link href="http://fonts.cdnfonts.com/css/uk-number-plate" rel="stylesheet"> <link href="http://fonts.cdnfonts.com/css/uk-number-plate" rel="stylesheet">
<!-- Inter font set used across the whole page --> <!-- Inter font set used across the whole page -->
<link rel="preconnect" href="https://rsms.me/"> <link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<title>WMGZON</title> <title>WMGZON</title>
</head> </head>
<body> <body>
{% include 'header.html' %} {% include 'header.html' %}
<div class="container"> <div class="container">
{% if category is defined %} {% if category is defined %}
{% set include_file = category+".html" %} {% set include_file = category+".html" %}
{% include include_file ignore missing %} {% include include_file ignore missing %}
{% endif %} {% endif %}
{% include content %} {% include content %}
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,14 +1,14 @@
<link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" /> <link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" />
<div id="input-form-wrap"> <div id="input-form-wrap">
<h2>Login</h2> <h2>Login</h2>
<form class="input-form" method="POST"> <form class="input-form" method="POST">
<input type="text" id="username" name="username" placeholder="Username" required> <input type="text" id="username" name="username" placeholder="Username" required>
<input type="password" id="password" name="password" placeholder="Password" required> <input type="password" id="password" name="password" placeholder="Password" required>
<input type="submit" id="login" value="Login"> <input type="submit" id="login" value="Login">
</form> </form>
<div id="create-account-wrap"> <div id="create-account-wrap">
<p>Not a member? <a href="signup">Create Account</a><p> <p>Not a member? <a href="signup">Create Account</a><p>
</div> </div>
</div> </div>

View File

@ -1,29 +1,29 @@
<link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" /> <link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" />
<div id="input-form-wrap"> <div id="input-form-wrap">
<h2>Create New Product</h2> <h2>Create New Product</h2>
<form class="input-form" method="POST" enctype="multipart/form-data"> <form class="input-form" method="POST" enctype="multipart/form-data">
<div class="input-form-row"> <div class="input-form-row">
<input type="text" id="name" name="name" placeholder="Product Name" required> <input type="text" id="name" name="name" placeholder="Product Name" required>
<input type="file" id="image" name="image" accept="image/x" required> <input type="file" id="image" name="image" accept="image/x" required>
</div> </div>
<textarea id="description" name="description" placeholder="Product Description" required></textarea> <textarea id="description" name="description" placeholder="Product Description" required></textarea>
<select name="category" id="category"> <select name="category" id="category">
{% for category in categories %} {% for category in categories %}
<option value="{{category.id}}">{{category.name}}</option> <option value="{{category.id}}">{{category.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="input-form-row"> <div class="input-form-row">
<input type="number" id="cost" name="cost" placeholder=10.99 min=0 step=any required> <input type="number" id="cost" name="cost" placeholder=10.99 min=0 step=any required>
<input type="number" id="quantity" name="quantity" placeholder=0 min=0 required> <input type="number" id="quantity" name="quantity" placeholder=0 min=0 required>
</div> </div>
<input type="submit" id="Create Product" value="Create Product"> <input type="submit" id="Create Product" value="Create Product">
</form> </form>
<div id="create-account-wrap"> <div id="create-account-wrap">
<p>Want to view all of your products? <a href="">Click Here</a><p> <p>Want to view all of your products? <a href="">Click Here</a><p>
</div> </div>
</div> </div>

View File

@ -1,65 +1,65 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/loginform.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/loginform.css') }}">
<div class="product-fs"> <div class="product-fs">
{% if product != None %} {% if product != None %}
{% if user.id == product.sellerID %} {% if user.id == product.sellerID %}
<form class="input-form" method="POST" action="/products/update/{{product.id}}" enctype="multipart/form-data"> <form class="input-form" method="POST" action="/products/update/{{product.id}}" enctype="multipart/form-data">
<div class="product-title"> <div class="product-title">
<input type="text" id="name" name="name" placeholder="Product Name" value="{{product.name}}" required> <input type="text" id="name" name="name" placeholder="Product Name" value="{{product.name}}" required>
</div> </div>
<div class="product-information"> <div class="product-information">
<div class="product-image"> <div class="product-image">
<img src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks" height="auto" width="150px" /> <img src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks" height="auto" width="150px" />
<input type="file" id="image" name="image" accept="image/x"> <input type="file" id="image" name="image" accept="image/x">
</div> </div>
<div> <div>
<div class="input-form-row"> <div class="input-form-row">
<input type="number" id="cost" name="cost" placeholder=10.99 min=0 step=any value="{{product.cost}}"required> <input type="number" id="cost" name="cost" placeholder=10.99 min=0 step=any value="{{product.cost}}"required>
<input type="textarea" id="description" name="description" placeholder="Product Description" value="{{product.description}}" required> <input type="textarea" id="description" name="description" placeholder="Product Description" value="{{product.description}}" required>
</div> </div>
<div class="input-form-row"> <div class="input-form-row">
<select name="category" id="category"> <select name="category" id="category">
{% for category in categories %} {% for category in categories %}
{% if category.id == product.category %} {% if category.id == product.category %}
<option value="{{category.id}}" selected>{{category.name}}</option> <option value="{{category.id}}" selected>{{category.name}}</option>
{% else %} {% else %}
<option value="{{category.id}}">{{category.name}}</option> <option value="{{category.id}}">{{category.name}}</option>
{% endif%} {% endif%}
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="input-form-row"> <div class="input-form-row">
<input type="number" id="quantity" name="quantity" placeholder=0 min=0 value="{{product.quantityAvailable}}" required> <input type="number" id="quantity" name="quantity" placeholder=0 min=0 value="{{product.quantityAvailable}}" required>
</div> </div>
</div> </div>
</div> </div>
<input type="submit" id="Create Product" value="Create Product"> <input type="submit" id="Create Product" value="Create Product">
</form> </form>
<div class="product-add-to-cart"></div> <div class="product-add-to-cart"></div>
{% else %} {% else %}
<img class="product-image" src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks"/> <img class="product-image" src="{{url_for('static', filename='assets/img/products/' + product.image)}}" alt="Brake Disks"/>
<div class="product-details"> <div class="product-details">
<div class="product-title">{{product.name}}</div> <div class="product-title">{{product.name}}</div>
<div class="product-description">{{product.description}}</div> <div class="product-description">{{product.description}}</div>
</div> </div>
<div class="product-acquisition-pane"> <div class="product-acquisition-pane">
<div class="product-price">£{{product.cost}}</div> <div class="product-price">£{{product.cost}}</div>
<div class="product-delivery">Earliest Delivery Friday 24th December</div> <div class="product-delivery">Earliest Delivery Friday 24th December</div>
<div class="product-postage">+£{{product.cost}} P&P</div> <div class="product-postage">+£{{product.cost}} P&P</div>
<div class="product-stock"> <div class="product-stock">
{% if product.quantityAvailable > 0 %} {% if product.quantityAvailable > 0 %}
<div class="product-instock">In Stock</div> <div class="product-instock">In Stock</div>
<div class="product-quantity">{{product.quantityAvailable}} Available</div> <div class="product-quantity">{{product.quantityAvailable}} Available</div>
<input type="submit" class="product-add-to-cart" value="Add to Cart" /> <input type="submit" class="product-add-to-cart" value="Add to Cart" />
{% else %} {% else %}
<div class="product-nostock">Out of Stock</div> <div class="product-nostock">Out of Stock</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,27 +1,27 @@
<link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" /> <link rel="stylesheet" href="{{url_for('static', filename='css/loginform.css')}}" />
<div id="input-form-wrap"> <div id="input-form-wrap">
<h2>Sign Up</h2> <h2>Sign Up</h2>
<form class="input-form" method="POST"> <form class="input-form" method="POST">
<div class="input-form-row"> <div class="input-form-row">
<input type="text" id="firstname" name="firstname" placeholder="First Name" required> <input type="text" id="firstname" name="firstname" placeholder="First Name" required>
<input type="text" id="lastname" name="lastname" placeholder="Last Name" required> <input type="text" id="lastname" name="lastname" placeholder="Last Name" required>
</div> </div>
<div class="input-form-row"> <div class="input-form-row">
<input type="text" id="username" name="username" placeholder="Username" required> <input type="text" id="username" name="username" placeholder="Username" required>
<input type="email" id="email" name="email" placeholder="Email Address" required> <input type="email" id="email" name="email" placeholder="Email Address" required>
</div> </div>
<input type="password" id="password" name="password" minlength=8 placeholder="Password" required> <input type="password" id="password" name="password" minlength=8 placeholder="Password" required>
<label class="checkbox">Signup as a Seller? <label class="checkbox">Signup as a Seller?
<input type="checkbox" id="seller" name="seller"/> <input type="checkbox" id="seller" name="seller"/>
<span class="checkmark"></span> <span class="checkmark"></span>
</label> </label>
<input type="submit" id="Sign Up" value="Sign Up"> <input type="submit" id="Sign Up" value="Sign Up">
</form> </form>
<div id="create-account-wrap"> <div id="create-account-wrap">
<p>Already have an account? <a href="login">Login</a><p> <p>Already have an account? <a href="login">Login</a><p>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import os import os
# Setup test environment variables # Setup test environment variables
os.environ["ENVIRON"] = "test" os.environ["ENVIRON"] = "test"

View File

@ -1,3 +1,3 @@
# Ensure test environment is set before using # Ensure test environment is set before using
# Runs the database creation scripts # Runs the database creation scripts
import scripts.create_database import scripts.create_database

View File

@ -1,70 +1,70 @@
import pytest import pytest
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from controllers.database.product import ProductController from controllers.database.product import ProductController
from models.products.product import Product from models.products.product import Product
product = Product( product = Product(
"product", "product",
"image.png", "image.png",
"description", "description",
10.00, 10.00,
1, 1,
1, 1,
datetime.now(), datetime.now(),
1 1
) )
# Tests a new product can be created # Tests a new product can be created
def test_create_product(): def test_create_product():
db = ProductController() db = ProductController()
db.create(product) db.create(product)
# Tests the database maintains integrity when we try # Tests the database maintains integrity when we try
# and add a product with the same details # and add a product with the same details
@pytest.mark.skip @pytest.mark.skip
def test_duplicate_product(): def test_duplicate_product():
db = ProductController() db = ProductController()
with pytest.raises(sqlite3.IntegrityError): with pytest.raises(sqlite3.IntegrityError):
db.create(product) db.create(product)
# Tests that products can be refined by category # Tests that products can be refined by category
def test_search_category(): def test_search_category():
db = ProductController() db = ProductController()
# Check each category for correct amount of test products # Check each category for correct amount of test products
assert len(db.read_all("Car Parts")) == 9 + 1 # Added in previous test assert len(db.read_all("Car Parts")) == 9 + 1 # Added in previous test
assert len(db.read_all("Books")) == 9 assert len(db.read_all("Books")) == 9
assert db.read_all("Phones") is None assert db.read_all("Phones") is None
# Tests that products can be refined by search term # Tests that products can be refined by search term
def test_search_term(): def test_search_term():
db = ProductController() db = ProductController()
# Check each search term for correct amount of test products # Check each search term for correct amount of test products
assert len(db.read_all(search_term="test")) == 33 assert len(db.read_all(search_term="test")) == 33
assert len(db.read_all("Car Parts", "test")) == 9 assert len(db.read_all("Car Parts", "test")) == 9
assert len(db.read_all(search_term="product")) == 1 assert len(db.read_all(search_term="product")) == 1
assert db.read_all(search_term="not_test") is None assert db.read_all(search_term="not_test") is None
# Test we the same product details get returned from the database # Test we the same product details get returned from the database
def test_read_product(): def test_read_product():
db = ProductController() db = ProductController()
# Test the same product is returned # Test the same product is returned
new_product = db.read("product") new_product = db.read("product")
assert isinstance(new_product, list) assert isinstance(new_product, list)
assert isinstance(new_product[0], Product) assert isinstance(new_product[0], Product)
# Update the ID on the item as database assigns new id # Update the ID on the item as database assigns new id
product.id = new_product[0].id product.id = new_product[0].id
assert new_product[0].__dict__ == product.__dict__ assert new_product[0].__dict__ == product.__dict__

View File

@ -1,74 +1,74 @@
import pytest import pytest
import sqlite3 import sqlite3
from controllers.database.user import UserController from controllers.database.user import UserController
from models.users.customer import Customer from models.users.customer import Customer
from models.users.seller import Seller from models.users.seller import Seller
customer = Customer( customer = Customer(
"testcustomer", "testcustomer",
"Password1", "Password1",
"firstname", "firstname",
"lastname", "lastname",
"test@test", "test@test",
"123456789" "123456789"
) )
seller = Seller( seller = Seller(
"testseller", "testseller",
"Password1", "Password1",
"firstname", "firstname",
"lastname", "lastname",
"seller@seller", "seller@seller",
"987654321" "987654321"
) )
# Tests a new user can be created # Tests a new user can be created
def test_create_user(): def test_create_user():
db = UserController() db = UserController()
db.create(customer) db.create(customer)
# Tests the database maintains integrity when we try # Tests the database maintains integrity when we try
# and add a user with the same details # and add a user with the same details
def test_duplicate_user(): def test_duplicate_user():
db = UserController() db = UserController()
with pytest.raises(sqlite3.IntegrityError): with pytest.raises(sqlite3.IntegrityError):
db.create(customer) db.create(customer)
# Test we the same user details get returned from the database # Test we the same user details get returned from the database
def test_read_user(): def test_read_user():
db = UserController() db = UserController()
# Test the same user is returned # Test the same user is returned
user = db.read(customer.username) user = db.read(customer.username)
assert isinstance(user, Customer) assert isinstance(user, Customer)
# Update the ID on the item as database assigns new id # Update the ID on the item as database assigns new id
customer.id = user.id customer.id = user.id
assert user.__dict__ == customer.__dict__ assert user.__dict__ == customer.__dict__
# Tests a new seller can be created # Tests a new seller can be created
def test_create_seller(): def test_create_seller():
db = UserController() db = UserController()
db.create(seller) db.create(seller)
# Test that the same seller details get returned from the database # Test that the same seller details get returned from the database
def test_read_seller(): def test_read_seller():
db = UserController() db = UserController()
# Test the same user is returned # Test the same user is returned
user = db.read(seller.username) user = db.read(seller.username)
assert isinstance(user, Seller) assert isinstance(user, Seller)
# Update the ID on the item as database assigns new id # Update the ID on the item as database assigns new id
seller.id = user.id seller.id = user.id
user.store = "" user.store = ""
assert user.__dict__ == seller.__dict__ assert user.__dict__ == seller.__dict__

View File

@ -1,11 +1,11 @@
import pycodestyle import pycodestyle
# Tests files to ensure they conform to pep8 standards # Tests files to ensure they conform to pep8 standards
def test_pep8_conformance(): def test_pep8_conformance():
"""Test that we conform to PEP8.""" """Test that we conform to PEP8."""
pep8style = pycodestyle.StyleGuide() pep8style = pycodestyle.StyleGuide()
dirs = ["./controllers", "./models", "./scripts", "./tests", "./utils"] dirs = ["./controllers", "./models", "./scripts", "./tests", "./utils"]
result = pep8style.check_files(dirs) result = pep8style.check_files(dirs)
assert result.total_errors == 0 assert result.total_errors == 0

View File

@ -1,46 +1,46 @@
import os import os
import uuid import uuid
import pathlib import pathlib
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename) -> bool: def allowed_file(filename) -> bool:
""" Ensures only filenames ending with the correct extension are allowed. """ Ensures only filenames ending with the correct extension are allowed.
Note: This does not verify that the content inside of the file Note: This does not verify that the content inside of the file
matches the type specified matches the type specified
""" """
return '.' in filename and \ return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def save_image(file) -> str | None: def save_image(file) -> str | None:
""" Saves a given file to disk with a random UUID4 generated """ Saves a given file to disk with a random UUID4 generated
filename. Returns the filename as a string. filename. Returns the filename as a string.
""" """
# Ensure that the correct file type is uploaded # Ensure that the correct file type is uploaded
if file is None or not allowed_file(file.filename): if file is None or not allowed_file(file.filename):
return None return None
# Create the product object and push to database # Create the product object and push to database
filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix
path = os.environ.get('FILESTORE') path = os.environ.get('FILESTORE')
file.save(os.path.join(path, filename)) file.save(os.path.join(path, filename))
return filename return filename
def create_directory(dir: str): def create_directory(dir: str):
""" Creates the given directory string is not alreay made """ """ Creates the given directory string is not alreay made """
try: try:
os.makedirs(dir) os.makedirs(dir)
except FileExistsError: except FileExistsError:
pass pass
def remove_file(dir: str): def remove_file(dir: str):
""" Removes a given file if it is present at the given dir """ """ Removes a given file if it is present at the given dir """
try: try:
os.remove(dir) os.remove(dir)
except FileNotFoundError: except FileNotFoundError:
pass pass

View File

@ -1,26 +1,26 @@
from flask import session from flask import session
from models.users.user import User from models.users.user import User
from controllers.database.user import UserController from controllers.database.user import UserController
def is_logged_in() -> User | None: def is_logged_in() -> User | None:
""" Returns the user object if the user is logged in """ Returns the user object if the user is logged in
Otherwise returns a None type Otherwise returns a None type
""" """
user_id = session.get('user_id') user_id = session.get('user_id')
if user_id is not None: if user_id is not None:
db = UserController() db = UserController()
return db.read_id(user_id) return db.read_id(user_id)
return None return None
def is_role(role: str) -> bool: def is_role(role: str) -> bool:
""" Function that returns true if the user is logged in as""" """ Function that returns true if the user is logged in as"""
user = is_logged_in() user = is_logged_in()
if user is not None: if user is not None:
return user.role == role return user.role == role
# User isn't logged in # User isn't logged in
return False return False