From 5a20a8d7c0ad7b1a244923501f45863f3268e9b6 Mon Sep 17 00:00:00 2001 From: Luke Else Date: Sun, 28 Jan 2024 11:59:04 +0000 Subject: [PATCH] Updated line endings to be in line with UNIX style --- .gitignore | 578 +++++++++++++++---------------- .gitlab-ci.yml | 20 +- README.md | 84 ++--- app.py | 54 +-- cicd/docker-compose.yml | 14 +- controllers/database/category.py | 126 +++---- controllers/database/database.py | 162 ++++----- controllers/database/product.py | 320 ++++++++--------- controllers/database/user.py | 170 ++++----- controllers/web/endpoints.py | 68 ++-- controllers/web/product.py | 376 ++++++++++---------- controllers/web/user.py | 198 +++++------ docker-compose.yml | 34 +- dockerfile | 12 +- models/category.py | 16 +- models/order.py | 30 +- models/products/carpart.py | 28 +- models/products/product.py | 76 ++-- models/users/admin.py | 30 +- models/users/customer.py | 30 +- models/users/seller.py | 32 +- models/users/user.py | 40 +-- requirements.txt | 50 +-- scripts/create_database.py | 102 +++--- scripts/create_tables.sql | 112 +++--- scripts/test_data.sql | 72 ++-- static/css/alerts.css | 144 ++++---- static/css/carparts.css | 136 ++++---- static/css/loginform.css | 308 ++++++++-------- static/css/products.css | 324 ++++++++--------- static/css/style.css | 348 +++++++++---------- templates/Car Parts.html | 44 +-- templates/content.html | 40 +-- templates/header.html | 102 +++--- templates/index.html | 54 +-- templates/login.html | 28 +- templates/new_product.html | 58 ++-- templates/product.html | 130 +++---- templates/signup.html | 54 +-- tests/__init__.py | 8 +- tests/database/__init__.py | 6 +- tests/database/test_products.py | 140 ++++---- tests/database/test_users.py | 148 ++++---- tests/general/test_pep8.py | 22 +- utils/file_utils.py | 92 ++--- utils/user_utils.py | 52 +-- 46 files changed, 2536 insertions(+), 2536 deletions(-) diff --git a/.gitignore b/.gitignore index 85263a3..8128775 100644 --- a/.gitignore +++ b/.gitignore @@ -1,289 +1,289 @@ -# 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 - - -### SQLite 3 Database ### -data/ -static/assets/img/products/ - -### CICD Registration files ### -cicd/runner-data - -### Flask ### -instance/* -!instance/.gitignore -.webassets-cache -.env - -### Flask.Python Stack ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# 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. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# 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: -# .python-version - -# pipenv -# 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 -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# 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 -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# 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 -# 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. -#.idea/ - -### Python ### -# Byte-compiled / optimized / DLL files - -# C extensions - -# Distribution / packaging - -# PyInstaller -# 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. - -# Installer logs - -# Unit test / coverage reports - -# Translations - -# Django stuff: - -# Flask stuff: - -# Scrapy stuff: - -# Sphinx documentation - -# PyBuilder - -# Jupyter Notebook - -# IPython - -# pyenv -# 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: -# .python-version - -# pipenv -# 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 -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. - -# poetry -# 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 -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control - -# pdm -# 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 -# in version control. -# https://pdm.fming.dev/#use-with-ide - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm - -# Celery stuff - -# SageMath parsed files - -# Environments - -# Spyder project settings - -# Rope project settings - -# mkdocs documentation - -# mypy - -# Pyre type checker - -# pytype static type analyzer - -# Cython debug symbols - -# PyCharm -# 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 -# 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. - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# End of 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 + + +### SQLite 3 Database ### +data/ +static/assets/img/products/ + +### CICD Registration files ### +cicd/runner-data + +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache +.env + +### Flask.Python Stack ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# 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. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# 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: +# .python-version + +# pipenv +# 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 +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# 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 +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# 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 +# 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. +#.idea/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# 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. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Django stuff: + +# Flask stuff: + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# Jupyter Notebook + +# IPython + +# pyenv +# 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: +# .python-version + +# pipenv +# 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 +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. + +# poetry +# 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 +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control + +# pdm +# 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 +# in version control. +# https://pdm.fming.dev/#use-with-ide + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm + +# Celery stuff + +# SageMath parsed files + +# Environments + +# Spyder project settings + +# Rope project settings + +# mkdocs documentation + +# mypy + +# Pyre type checker + +# pytype static type analyzer + +# Cython debug symbols + +# PyCharm +# 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 +# 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. + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,flask,web diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 60e6086..0d979f7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,11 @@ -variables: - ENVIRON: "test" - -pytest: - stage: test - script: - - cd /builds/u5500327/wmgzon - # - python -m venv .venv - # - source /bin/activate - - pip install -r requirements.txt +variables: + ENVIRON: "test" + +pytest: + stage: test + script: + - cd /builds/u5500327/wmgzon + # - python -m venv .venv + # - source /bin/activate + - pip install -r requirements.txt - pytest --disable-warnings \ No newline at end of file diff --git a/README.md b/README.md index fcc19a4..3497f07 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,43 @@ ---- -runme: - id: 01HK0BF4BTBSKR9VWAP1KGD2S7 - version: v2.0 ---- - -# WMGZON - -`Flask web application serving WMGZON and its relevant backend services.` - -## Initialisation - -To start you need to create a virtual environment and load in the required dependencies for the project - -```sh {"closeTerminalOnSuccess":"false","id":"01HK0BJCK9BR05J127F1X0RZP9"} -python -m venv .venv -./.venv/Scripts/Activate.ps1 -pip install -r requirements.txt -``` - -## Testing - -To run the full suite of unit tests for the webapp simply run the following command in the venv -```sh -pytest -``` - -## Running - -### Pre-Requisites - -- Docker daemon is installed and running. -- Docker compose is installed. - -### Instructions - -In order to run the web app, simply use the command - -```sh {"id":"01HKD0VRADDYQ92W22JN1FHA03"} -docker-compose up -d -``` - +--- +runme: + id: 01HK0BF4BTBSKR9VWAP1KGD2S7 + version: v2.0 +--- + +# WMGZON + +`Flask web application serving WMGZON and its relevant backend services.` + +## Initialisation + +To start you need to create a virtual environment and load in the required dependencies for the project + +```sh {"closeTerminalOnSuccess":"false","id":"01HK0BJCK9BR05J127F1X0RZP9"} +python -m venv .venv +./.venv/Scripts/Activate.ps1 +pip install -r requirements.txt +``` + +## Testing + +To run the full suite of unit tests for the webapp simply run the following command in the venv +```sh +pytest +``` + +## Running + +### Pre-Requisites + +- Docker daemon is installed and running. +- Docker compose is installed. + +### Instructions + +In order to run the web app, simply use the command + +```sh {"id":"01HKD0VRADDYQ92W22JN1FHA03"} +docker-compose up -d +``` + to run the container in a detatched mode. \ No newline at end of file diff --git a/app.py b/app.py index 831c964..053af50 100644 --- a/app.py +++ b/app.py @@ -1,27 +1,27 @@ -from flask import Flask -from os import environ -from controllers.web.endpoints import blueprint - -''' - Main entrypoint for Flask application. - Initialises any components that are needed at runtime such as the - Database manager... -''' - -app = Flask(__name__) - -# Set app secret key to sign session cookies -secret_key = environ.get("APPSECRET") -if secret_key is None: - # NO Secret Key set! - print("No app secret set, please set one before deploying in production") - app.secret_key = "DEFAULTKEY" -else: - app.secret_key = secret_key - -# Register a blueprint -app.register_blueprint(blueprint) - -if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0", port=8080) - +from flask import Flask +from os import environ +from controllers.web.endpoints import blueprint + +''' + Main entrypoint for Flask application. + Initialises any components that are needed at runtime such as the + Database manager... +''' + +app = Flask(__name__) + +# Set app secret key to sign session cookies +secret_key = environ.get("APPSECRET") +if secret_key is None: + # NO Secret Key set! + print("No app secret set, please set one before deploying in production") + app.secret_key = "DEFAULTKEY" +else: + app.secret_key = secret_key + +# Register a blueprint +app.register_blueprint(blueprint) + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=8080) + diff --git a/cicd/docker-compose.yml b/cicd/docker-compose.yml index 6d2727b..34f82de 100644 --- a/cicd/docker-compose.yml +++ b/cicd/docker-compose.yml @@ -1,8 +1,8 @@ -version: '3.8' -services: - gitlab-runner: - image: gitlab/gitlab-runner:latest - volumes: - - ./runner-data:/etc/gitlab-runner - - /var/run/docker.sock:/var/run/docker.sock +version: '3.8' +services: + gitlab-runner: + image: gitlab/gitlab-runner:latest + volumes: + - ./runner-data:/etc/gitlab-runner + - /var/run/docker.sock:/var/run/docker.sock restart: unless-stopped \ No newline at end of file diff --git a/controllers/database/category.py b/controllers/database/category.py index 71fa9b3..ab02aad 100644 --- a/controllers/database/category.py +++ b/controllers/database/category.py @@ -1,63 +1,63 @@ -from .database import DatabaseController -from models.category import Category - - -class CategoryController(DatabaseController): - FIELDS = ['id', 'name'] - - def __init__(self): - super().__init__() - - def create(self, category: Category): - params = [ - category.name, - ] - - self._conn.execute( - "INSERT INTO Categories (name) VALUES (?)", - params - ) - self._conn.commit() - - def read(self, id: int = 0) -> Category | None: - params = [ - id - ] - - cursor = self._conn.execute( - "SELECT * FROM Categories WHERE id = ?", - params - ) - row = cursor.fetchone() - - if row is None: - return None - - params = dict(zip(self.FIELDS, row)) - obj = self.new_instance(Category, params) - - return obj - - def read_all(self) -> list[Category] | None: - cursor = self._conn.execute( - "SELECT * FROM Categories", - ) - rows = cursor.fetchall() - - if rows is None: - return None - - categories = list() - - for category in rows: - params = dict(zip(self.FIELDS, category)) - obj = self.new_instance(Category, params) - categories.append(obj) - - return categories - - def update(self): - print("Doing work") - - def delete(self): - print("Doing work") +from .database import DatabaseController +from models.category import Category + + +class CategoryController(DatabaseController): + FIELDS = ['id', 'name'] + + def __init__(self): + super().__init__() + + def create(self, category: Category): + params = [ + category.name, + ] + + self._conn.execute( + "INSERT INTO Categories (name) VALUES (?)", + params + ) + self._conn.commit() + + def read(self, id: int = 0) -> Category | None: + params = [ + id + ] + + cursor = self._conn.execute( + "SELECT * FROM Categories WHERE id = ?", + params + ) + row = cursor.fetchone() + + if row is None: + return None + + params = dict(zip(self.FIELDS, row)) + obj = self.new_instance(Category, params) + + return obj + + def read_all(self) -> list[Category] | None: + cursor = self._conn.execute( + "SELECT * FROM Categories", + ) + rows = cursor.fetchall() + + if rows is None: + return None + + categories = list() + + for category in rows: + params = dict(zip(self.FIELDS, category)) + obj = self.new_instance(Category, params) + categories.append(obj) + + return categories + + def update(self): + print("Doing work") + + def delete(self): + print("Doing work") diff --git a/controllers/database/database.py b/controllers/database/database.py index fe0fc66..f4c76ca 100644 --- a/controllers/database/database.py +++ b/controllers/database/database.py @@ -1,81 +1,81 @@ -from abc import ABC, abstractmethod -from typing import Mapping, Any -import sqlite3 -import os - - -class DatabaseController(ABC): - """ Abstract Base Class to handle database access for each component - in the web app - """ - - __data_dir = "./data/" - __db_name = "wmgzon.db" - - # Use test file if necessary - if os.environ.get("ENVIRON") == "test": - __db_name = "test_" + __db_name - - __sqlitefile = __data_dir + __db_name - - def __init__(self): - """ Initialises the object and creates a connection to the local - DB on the server - """ - self._conn = None - try: - # Creates a connection and specifies a flag to parse all types - # back down into Python declared types e.g. date & time - self._conn = sqlite3.connect( - self.__sqlitefile, detect_types=sqlite3.PARSE_DECLTYPES) - except sqlite3.Error as e: - # Close the connection if still open - if self._conn: - self._conn.close() - print(e) - - def __del__(self): - """ Object Destructor which kills the connection to the database """ - if self._conn is not None: - self._conn.close() - - def new_instance(self, of: type, with_fields: Mapping[str, Any]): - """ Takes a dictionary of fields and returns the object - with those fields populated - """ - obj = of.__new__(of) - for attr, value in with_fields.items(): - setattr(obj, attr, value) - return obj - - """ - Set of CRUD methods to allow for Data manipulation on the backend - """ - - @abstractmethod - def create(self): - """ Abstract method used to create a new record of a given - type within the database - """ - pass - - @abstractmethod - def read(self): - """ Abstract method used to read a record of a given - type from the database - """ - pass - - @abstractmethod - def update(self): - """ Abstract method used to update a record of a given - type within the database - """ - pass - - @abstractmethod - def delete(self): - """ Abstract method used to delete record of a given - type from the database - """ - pass +from abc import ABC, abstractmethod +from typing import Mapping, Any +import sqlite3 +import os + + +class DatabaseController(ABC): + """ Abstract Base Class to handle database access for each component + in the web app + """ + + __data_dir = "./data/" + __db_name = "wmgzon.db" + + # Use test file if necessary + if os.environ.get("ENVIRON") == "test": + __db_name = "test_" + __db_name + + __sqlitefile = __data_dir + __db_name + + def __init__(self): + """ Initialises the object and creates a connection to the local + DB on the server + """ + self._conn = None + try: + # Creates a connection and specifies a flag to parse all types + # back down into Python declared types e.g. date & time + self._conn = sqlite3.connect( + self.__sqlitefile, detect_types=sqlite3.PARSE_DECLTYPES) + except sqlite3.Error as e: + # Close the connection if still open + if self._conn: + self._conn.close() + print(e) + + def __del__(self): + """ Object Destructor which kills the connection to the database """ + if self._conn is not None: + self._conn.close() + + def new_instance(self, of: type, with_fields: Mapping[str, Any]): + """ Takes a dictionary of fields and returns the object + with those fields populated + """ + obj = of.__new__(of) + for attr, value in with_fields.items(): + setattr(obj, attr, value) + return obj + + """ + Set of CRUD methods to allow for Data manipulation on the backend + """ + + @abstractmethod + def create(self): + """ Abstract method used to create a new record of a given + type within the database + """ + pass + + @abstractmethod + def read(self): + """ Abstract method used to read a record of a given + type from the database + """ + pass + + @abstractmethod + def update(self): + """ Abstract method used to update a record of a given + type within the database + """ + pass + + @abstractmethod + def delete(self): + """ Abstract method used to delete record of a given + type from the database + """ + pass diff --git a/controllers/database/product.py b/controllers/database/product.py index d6c35a4..469f49a 100644 --- a/controllers/database/product.py +++ b/controllers/database/product.py @@ -1,160 +1,160 @@ -from .database import DatabaseController -from models.products.product import Product - - -class ProductController(DatabaseController): - FIELDS = ['id', 'name', 'image', 'description', 'cost', - 'sellerID', 'category', 'postedDate', 'quantityAvailable'] - - def __init__(self): - super().__init__() - - def create(self, product: Product): - params = [ - product.name, - product.image, - product.description, - product.cost, - product.category, - product.sellerID, - product.postedDate, - product.quantityAvailable - ] - - self._conn.execute( - """ - INSERT INTO Products - (name, image, description, cost, categoryID, - sellerID, postedDate, quantityAvailable) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - params - ) - self._conn.commit() - - def read(self, name: str = "") -> list[Product] | None: - params = [ - "%" + name + "%" - ] - - cursor = self._conn.execute( - "SELECT * FROM Products WHERE name like ?", - params - ) - rows = cursor.fetchmany() - - if rows is None: - return None - - products = list() - - # Create an object for each row - for product in rows: - params = dict(zip(self.FIELDS, product)) - obj = self.new_instance(Product, params) - products.append(obj) - - return products - - def read_id(self, id: int) -> Product | None: - params = [ - id - ] - - cursor = self._conn.execute( - "SELECT * FROM Products WHERE id == ?", - params - ) - row = cursor.fetchone() - - if row is None: - return None - - # Create an object with the row - params = dict(zip(self.FIELDS, row)) - obj = self.new_instance(Product, params) - - return obj - - def read_all(self, category: str = "", - search_term: str = "") -> list[Product] | None: - params = [ - "%" + category + "%", - "%" + search_term + "%" - ] - - cursor = self._conn.execute( - """SELECT * FROM Products - INNER JOIN Categories ON Products.categoryID = Categories.id - WHERE Categories.name LIKE ? - AND Products.name LIKE ? - """, - params - ) - rows = cursor.fetchall() - - if len(rows) == 0: - return None - - products = list() - - # Create an object for each row - for product in rows: - params = dict(zip(self.FIELDS, product)) - obj = self.new_instance(Product, params) - products.append(obj) - - return products - - def read_user(self, user_id: int) -> list[Product] | None: - params = [ - user_id - ] - - cursor = self._conn.execute( - """SELECT * FROM Products - WHERE sellerID = ? - """, - params - ) - rows = cursor.fetchall() - - if len(rows) == 0: - return None - - products = list() - - # Create an object for each row - for product in rows: - params = dict(zip(self.FIELDS, product)) - obj = self.new_instance(Product, params) - products.append(obj) - - return products - - def update(self, product: Product): - params = [ - product.name, - product.description, - product.image, - product.cost, - product.quantityAvailable, - product.category, - product.id - ] - - cursor = self._conn.execute( - """UPDATE Products - SET name = ?, - description = ?, - image = ?, - cost = ?, - quantityAvailable = ?, - categoryID = ? - WHERE id = ? - """, - params - ) - self._conn.commit() - - def delete(self): - print("Doing work") +from .database import DatabaseController +from models.products.product import Product + + +class ProductController(DatabaseController): + FIELDS = ['id', 'name', 'image', 'description', 'cost', + 'sellerID', 'category', 'postedDate', 'quantityAvailable'] + + def __init__(self): + super().__init__() + + def create(self, product: Product): + params = [ + product.name, + product.image, + product.description, + product.cost, + product.category, + product.sellerID, + product.postedDate, + product.quantityAvailable + ] + + self._conn.execute( + """ + INSERT INTO Products + (name, image, description, cost, categoryID, + sellerID, postedDate, quantityAvailable) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + params + ) + self._conn.commit() + + def read(self, name: str = "") -> list[Product] | None: + params = [ + "%" + name + "%" + ] + + cursor = self._conn.execute( + "SELECT * FROM Products WHERE name like ?", + params + ) + rows = cursor.fetchmany() + + if rows is None: + return None + + products = list() + + # Create an object for each row + for product in rows: + params = dict(zip(self.FIELDS, product)) + obj = self.new_instance(Product, params) + products.append(obj) + + return products + + def read_id(self, id: int) -> Product | None: + params = [ + id + ] + + cursor = self._conn.execute( + "SELECT * FROM Products WHERE id == ?", + params + ) + row = cursor.fetchone() + + if row is None: + return None + + # Create an object with the row + params = dict(zip(self.FIELDS, row)) + obj = self.new_instance(Product, params) + + return obj + + def read_all(self, category: str = "", + search_term: str = "") -> list[Product] | None: + params = [ + "%" + category + "%", + "%" + search_term + "%" + ] + + cursor = self._conn.execute( + """SELECT * FROM Products + INNER JOIN Categories ON Products.categoryID = Categories.id + WHERE Categories.name LIKE ? + AND Products.name LIKE ? + """, + params + ) + rows = cursor.fetchall() + + if len(rows) == 0: + return None + + products = list() + + # Create an object for each row + for product in rows: + params = dict(zip(self.FIELDS, product)) + obj = self.new_instance(Product, params) + products.append(obj) + + return products + + def read_user(self, user_id: int) -> list[Product] | None: + params = [ + user_id + ] + + cursor = self._conn.execute( + """SELECT * FROM Products + WHERE sellerID = ? + """, + params + ) + rows = cursor.fetchall() + + if len(rows) == 0: + return None + + products = list() + + # Create an object for each row + for product in rows: + params = dict(zip(self.FIELDS, product)) + obj = self.new_instance(Product, params) + products.append(obj) + + return products + + def update(self, product: Product): + params = [ + product.name, + product.description, + product.image, + product.cost, + product.quantityAvailable, + product.category, + product.id + ] + + cursor = self._conn.execute( + """UPDATE Products + SET name = ?, + description = ?, + image = ?, + cost = ?, + quantityAvailable = ?, + categoryID = ? + WHERE id = ? + """, + params + ) + self._conn.commit() + + def delete(self): + print("Doing work") diff --git a/controllers/database/user.py b/controllers/database/user.py index 04d7692..6f0c09d 100644 --- a/controllers/database/user.py +++ b/controllers/database/user.py @@ -1,85 +1,85 @@ -from .database import DatabaseController -from models.users.user import User -from models.users.customer import Customer -from models.users.seller import Seller - - -class UserController(DatabaseController): - FIELDS = ['id', 'username', 'password', 'firstName', - 'lastName', 'email', 'phone', 'role'] - - def __init__(self): - super().__init__() - - def create(self, user: User): - params = [ - user.username, - user.password, - user.firstName, - user.lastName, - user.email, - user.phone, - user.role - ] - - self._conn.execute( - """INSERT INTO Users - (username, password, first_name, last_name, email, phone, role) - VALUES (?, ?, ?, ?, ?, ?, ?)""", - params - ) - self._conn.commit() - - def read(self, username: str) -> User | None: - params = [ - username - ] - - cursor = self._conn.execute( - "SELECT * FROM Users WHERE Username = ?", - params - ) - row = cursor.fetchone() - - if row is not None: - params = dict(zip(self.FIELDS, row)) - - # Is user a seller - type = Customer - if row[7] == "Seller": - type = Seller - - obj = self.new_instance(type, params) - return obj - - return None - - def read_id(self, id: int) -> User | None: - params = [ - id - ] - - cursor = self._conn.execute( - "SELECT * FROM Users WHERE id = ?", - params - ) - row = cursor.fetchone() - - if row is not None: - params = dict(zip(self.FIELDS, row)) - - # Is user a seller - type = Customer - if row[7] == "Seller": - type = Seller - - obj = self.new_instance(type, params) - return obj - - return None - - def update(self): - print("Doing work") - - def delete(self): - print("Doing work") +from .database import DatabaseController +from models.users.user import User +from models.users.customer import Customer +from models.users.seller import Seller + + +class UserController(DatabaseController): + FIELDS = ['id', 'username', 'password', 'firstName', + 'lastName', 'email', 'phone', 'role'] + + def __init__(self): + super().__init__() + + def create(self, user: User): + params = [ + user.username, + user.password, + user.firstName, + user.lastName, + user.email, + user.phone, + user.role + ] + + self._conn.execute( + """INSERT INTO Users + (username, password, first_name, last_name, email, phone, role) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + params + ) + self._conn.commit() + + def read(self, username: str) -> User | None: + params = [ + username + ] + + cursor = self._conn.execute( + "SELECT * FROM Users WHERE Username = ?", + params + ) + row = cursor.fetchone() + + if row is not None: + params = dict(zip(self.FIELDS, row)) + + # Is user a seller + type = Customer + if row[7] == "Seller": + type = Seller + + obj = self.new_instance(type, params) + return obj + + return None + + def read_id(self, id: int) -> User | None: + params = [ + id + ] + + cursor = self._conn.execute( + "SELECT * FROM Users WHERE id = ?", + params + ) + row = cursor.fetchone() + + if row is not None: + params = dict(zip(self.FIELDS, row)) + + # Is user a seller + type = Customer + if row[7] == "Seller": + type = Seller + + obj = self.new_instance(type, params) + return obj + + return None + + def update(self): + print("Doing work") + + def delete(self): + print("Doing work") diff --git a/controllers/web/endpoints.py b/controllers/web/endpoints.py index 1aaed23..fc5f753 100644 --- a/controllers/web/endpoints.py +++ b/controllers/web/endpoints.py @@ -1,34 +1,34 @@ -from models.users.user import User -from controllers.database.user import UserController - -from flask import redirect, Blueprint, session - -from . import user -from . import product - -blueprint = Blueprint('main', __name__) - -blueprint.register_blueprint(user.blueprint) -blueprint.register_blueprint(product.blueprint) - - -# CONTEXTS # - -# Function that returns a given user class based on the ID in the session -@blueprint.context_processor -def get_user() -> dict[User | None]: - # Get the user based on the user ID - user_id = session.get('user_id') - user = None - - if user_id is not None: - db = UserController() - user = db.read_id(user_id) - - return dict(user=user) - - -# Function responsible for displaying the main landing page of the site -@blueprint.route('/') -def index(): - return redirect("/products") +from models.users.user import User +from controllers.database.user import UserController + +from flask import redirect, Blueprint, session + +from . import user +from . import product + +blueprint = Blueprint('main', __name__) + +blueprint.register_blueprint(user.blueprint) +blueprint.register_blueprint(product.blueprint) + + +# CONTEXTS # + +# Function that returns a given user class based on the ID in the session +@blueprint.context_processor +def get_user() -> dict[User | None]: + # Get the user based on the user ID + user_id = session.get('user_id') + user = None + + if user_id is not None: + db = UserController() + user = db.read_id(user_id) + + return dict(user=user) + + +# Function responsible for displaying the main landing page of the site +@blueprint.route('/') +def index(): + return redirect("/products") diff --git a/controllers/web/product.py b/controllers/web/product.py index 5a3bec5..4b983ef 100644 --- a/controllers/web/product.py +++ b/controllers/web/product.py @@ -1,188 +1,188 @@ -""" - Product related endpoints. Included contexts for principles such as - categories and image processing. -""" - -from flask import render_template, session, flash, request, redirect, Blueprint - -from models.products.product import Product -from controllers.database.product import ProductController -from controllers.database.category import CategoryController -from controllers.database.user import UserController - -from datetime import datetime -from utils.file_utils import allowed_file, save_image, remove_file -from utils.user_utils import is_role - -import pathlib -import os - -blueprint = Blueprint("products", __name__, url_prefix="/products") - - -@blueprint.context_processor -def category_list(): - """ Places a list of all categories in the products context """ - database = CategoryController() - categories = database.read_all() - return dict(categories=categories) - - -@blueprint.route('/') -def index(): - """ The front product page """ - # Returning an empty category acts the same - # as a generic home page - return category("") - - -@blueprint.route('/') -def category(category: str): - """ Loads a given categories page """ - database = ProductController() - - # Check to see if there is a custome search term - search_term = request.args.get("search", type=str) - if search_term is not None: - products = database.read_all(category, search_term) - else: - products = database.read_all(category) - - # No Products visible - if products is None: - flash( - f"No Products available. Try expanding your search criteria.", - "warning" - ) - - return render_template( - 'index.html', - content="content.html", - products=products, - category=category - ) - - -@blueprint.route('/') -def id(id: int): - """ Loads a given product based on ID """ - db = ProductController() - product = db.read_id(id) - - # Check that a valid product was returned - if product is None: - flash(f"No Product available with id {id}", "warning") - return redirect("/") - - print(product.name) - return render_template( - 'index.html', - content='product.html', - product=product - ) - - -@blueprint.route('/add') -def display_add_product(): - """ Launches the page to add a new product to the site """ - user_id = session.get('user_id') - - # User needs to be logged in as a seller to view this page - if not is_role("Seller"): - flash("You must be logged in as a seller to view this page!", "error") - return redirect("/") - - return render_template('index.html', content='new_product.html') - - -@blueprint.post('/add') -def add_product(): - """ Server site processing to handle a request to add a - new product to the site - """ - user_id = session.get('user_id') - - # User needs to be logged in as a seller to view this page - if not is_role("Seller"): - flash("You must be logged in as a seller to view this page!", "error") - return redirect("/") - - file = request.files.get('image') - image_filename = save_image(file) - - product = Product( - request.form.get('name'), - image_filename if image_filename is not None else "", - request.form.get('description'), - request.form.get('cost'), - request.form.get('category'), - user_id, - datetime.now(), - request.form.get('quantity') - ) - - db = ProductController() - db.create(product) - - return redirect('/products/ownproducts') - - -@blueprint.post('/update/') -def update_product(id: int): - """ Processes a request to update a product in place on the site """ - # Ensure that the product belongs to the current user - user_id = session.get('user_id') - - # User needs to be logged in as a seller to view this page - if not is_role("Seller"): - flash("You must be logged in as a seller to view this page!", "error") - return redirect("/") - - db = ProductController() - product = db.read_id(id) - - if product.sellerID != user_id: - flash("This product does not belong to you!", "error") - return redirect("/ownproducts") - - # Save new image file - file = request.files.get('image') - new_image = save_image(file) - - if new_image is not None: - remove_file(os.path.join(os.environ.get('FILESTORE'), product.image)) - product.image = new_image - - # Update product details - product.name = request.form.get('name') - product.description = request.form.get('description') - product.category = request.form.get('category') - product.cost = request.form.get('cost') - product.quantityAvailable = request.form.get('quantity') - - db.update(product) - flash("Product successfully updated", 'notice') - return redirect(f"/products/{product.id}") - - -@blueprint.route('/ownproducts') -def display_own_products(): - """ Display products owned by the currently logged in seller """ - user_id = session.get('user_id') - - # User must be logged in as seller to view page - if not is_role("Seller"): - flash("You must be logged in as a seller to view this page!", "error") - return redirect("/") - - db = ProductController() - products = db.read_user(user_id) - - if products is None: - flash("You don't currently have any products for sale.", "info") - - return render_template( - 'index.html', - content='content.html', - products=products - ) +""" + Product related endpoints. Included contexts for principles such as + categories and image processing. +""" + +from flask import render_template, session, flash, request, redirect, Blueprint + +from models.products.product import Product +from controllers.database.product import ProductController +from controllers.database.category import CategoryController +from controllers.database.user import UserController + +from datetime import datetime +from utils.file_utils import allowed_file, save_image, remove_file +from utils.user_utils import is_role + +import pathlib +import os + +blueprint = Blueprint("products", __name__, url_prefix="/products") + + +@blueprint.context_processor +def category_list(): + """ Places a list of all categories in the products context """ + database = CategoryController() + categories = database.read_all() + return dict(categories=categories) + + +@blueprint.route('/') +def index(): + """ The front product page """ + # Returning an empty category acts the same + # as a generic home page + return category("") + + +@blueprint.route('/') +def category(category: str): + """ Loads a given categories page """ + database = ProductController() + + # Check to see if there is a custome search term + search_term = request.args.get("search", type=str) + if search_term is not None: + products = database.read_all(category, search_term) + else: + products = database.read_all(category) + + # No Products visible + if products is None: + flash( + f"No Products available. Try expanding your search criteria.", + "warning" + ) + + return render_template( + 'index.html', + content="content.html", + products=products, + category=category + ) + + +@blueprint.route('/') +def id(id: int): + """ Loads a given product based on ID """ + db = ProductController() + product = db.read_id(id) + + # Check that a valid product was returned + if product is None: + flash(f"No Product available with id {id}", "warning") + return redirect("/") + + print(product.name) + return render_template( + 'index.html', + content='product.html', + product=product + ) + + +@blueprint.route('/add') +def display_add_product(): + """ Launches the page to add a new product to the site """ + user_id = session.get('user_id') + + # User needs to be logged in as a seller to view this page + if not is_role("Seller"): + flash("You must be logged in as a seller to view this page!", "error") + return redirect("/") + + return render_template('index.html', content='new_product.html') + + +@blueprint.post('/add') +def add_product(): + """ Server site processing to handle a request to add a + new product to the site + """ + user_id = session.get('user_id') + + # User needs to be logged in as a seller to view this page + if not is_role("Seller"): + flash("You must be logged in as a seller to view this page!", "error") + return redirect("/") + + file = request.files.get('image') + image_filename = save_image(file) + + product = Product( + request.form.get('name'), + image_filename if image_filename is not None else "", + request.form.get('description'), + request.form.get('cost'), + request.form.get('category'), + user_id, + datetime.now(), + request.form.get('quantity') + ) + + db = ProductController() + db.create(product) + + return redirect('/products/ownproducts') + + +@blueprint.post('/update/') +def update_product(id: int): + """ Processes a request to update a product in place on the site """ + # Ensure that the product belongs to the current user + user_id = session.get('user_id') + + # User needs to be logged in as a seller to view this page + if not is_role("Seller"): + flash("You must be logged in as a seller to view this page!", "error") + return redirect("/") + + db = ProductController() + product = db.read_id(id) + + if product.sellerID != user_id: + flash("This product does not belong to you!", "error") + return redirect("/ownproducts") + + # Save new image file + file = request.files.get('image') + new_image = save_image(file) + + if new_image is not None: + remove_file(os.path.join(os.environ.get('FILESTORE'), product.image)) + product.image = new_image + + # Update product details + product.name = request.form.get('name') + product.description = request.form.get('description') + product.category = request.form.get('category') + product.cost = request.form.get('cost') + product.quantityAvailable = request.form.get('quantity') + + db.update(product) + flash("Product successfully updated", 'notice') + return redirect(f"/products/{product.id}") + + +@blueprint.route('/ownproducts') +def display_own_products(): + """ Display products owned by the currently logged in seller """ + user_id = session.get('user_id') + + # User must be logged in as seller to view page + if not is_role("Seller"): + flash("You must be logged in as a seller to view this page!", "error") + return redirect("/") + + db = ProductController() + products = db.read_user(user_id) + + if products is None: + flash("You don't currently have any products for sale.", "info") + + return render_template( + 'index.html', + content='content.html', + products=products + ) diff --git a/controllers/web/user.py b/controllers/web/user.py index 60bd316..5f2eb58 100644 --- a/controllers/web/user.py +++ b/controllers/web/user.py @@ -1,99 +1,99 @@ -""" The user controller to manage all of the user related endpoints - in the web app -""" -from flask import Blueprint - -from flask import render_template, redirect, request, session, flash -from controllers.database.user import UserController -from models.users.user import User -from models.users.customer import Customer -from models.users.seller import Seller -from hashlib import sha512 - -# Blueprint to append user endpoints to -blueprint = Blueprint("users", __name__) - - -# LOGIN FUNCTIONALITY -@blueprint.route('/login') -def display_login(): - """ Function responsible for delivering the Login page for the site """ - return render_template('index.html', content="login.html") - - -@blueprint.post('/login') -def login(): - """ Function to handle the backend processing of a login request """ - database = UserController() - user = database.read(request.form['username']) - error = None - - # No user found - if user is None: - error = "No user found with the username " + request.form['username'] - flash(error, 'warning') - return redirect("/login") - - # Incorrect Password - if sha512(request.form['password'].encode()).hexdigest() != user.password: - error = "Incorrect Password" - flash(error, 'warning') - return redirect("/login") - - session['user_id'] = user.id - return redirect("/") - - -# SIGNUP FUNCTIONALITY -@blueprint.route('/signup') -def display_signup(): - """ Function responsible for delivering the Signup page for the site """ - return render_template('index.html', content="signup.html") - - -@blueprint.post('/signup') -def signup(): - """ Function to handle the backend processing of a signup request """ - database = UserController() - - # User already exists - if database.read(request.form['username']) is not None: - error = "User, " + request.form['username'] + " already exists" - flash(error, 'warning') - return redirect("/signup") - - # Signup as Seller or Customer - if request.form.get('seller'): - user = Seller( - request.form['username'], - # Hashed as soon as it is recieved on the backend - sha512(request.form['password'].encode()).hexdigest(), - request.form['firstname'], - request.form['lastname'], - request.form['email'], - "123" - ) - else: - user = Customer( - request.form['username'], - # Hashed as soon as it is recieved on the backend - sha512(request.form['password'].encode()).hexdigest(), - request.form['firstname'], - request.form['lastname'], - request.form['email'], - "123" - ) - - database.create(user) - - # Code 307 Preserves the original request (POST) - return redirect("/login", code=307) - - -# SIGN OUT FUNCTIONALITY -@blueprint.route('/logout') -def logout(): - """ Function responsible for handling logouts from the site """ - # Clear the current user from the session if they are logged in - session.pop('user_id', None) - return redirect("/") +""" The user controller to manage all of the user related endpoints + in the web app +""" +from flask import Blueprint + +from flask import render_template, redirect, request, session, flash +from controllers.database.user import UserController +from models.users.user import User +from models.users.customer import Customer +from models.users.seller import Seller +from hashlib import sha512 + +# Blueprint to append user endpoints to +blueprint = Blueprint("users", __name__) + + +# LOGIN FUNCTIONALITY +@blueprint.route('/login') +def display_login(): + """ Function responsible for delivering the Login page for the site """ + return render_template('index.html', content="login.html") + + +@blueprint.post('/login') +def login(): + """ Function to handle the backend processing of a login request """ + database = UserController() + user = database.read(request.form['username']) + error = None + + # No user found + if user is None: + error = "No user found with the username " + request.form['username'] + flash(error, 'warning') + return redirect("/login") + + # Incorrect Password + if sha512(request.form['password'].encode()).hexdigest() != user.password: + error = "Incorrect Password" + flash(error, 'warning') + return redirect("/login") + + session['user_id'] = user.id + return redirect("/") + + +# SIGNUP FUNCTIONALITY +@blueprint.route('/signup') +def display_signup(): + """ Function responsible for delivering the Signup page for the site """ + return render_template('index.html', content="signup.html") + + +@blueprint.post('/signup') +def signup(): + """ Function to handle the backend processing of a signup request """ + database = UserController() + + # User already exists + if database.read(request.form['username']) is not None: + error = "User, " + request.form['username'] + " already exists" + flash(error, 'warning') + return redirect("/signup") + + # Signup as Seller or Customer + if request.form.get('seller'): + user = Seller( + request.form['username'], + # Hashed as soon as it is recieved on the backend + sha512(request.form['password'].encode()).hexdigest(), + request.form['firstname'], + request.form['lastname'], + request.form['email'], + "123" + ) + else: + user = Customer( + request.form['username'], + # Hashed as soon as it is recieved on the backend + sha512(request.form['password'].encode()).hexdigest(), + request.form['firstname'], + request.form['lastname'], + request.form['email'], + "123" + ) + + database.create(user) + + # Code 307 Preserves the original request (POST) + return redirect("/login", code=307) + + +# SIGN OUT FUNCTIONALITY +@blueprint.route('/logout') +def logout(): + """ Function responsible for handling logouts from the site """ + # Clear the current user from the session if they are logged in + session.pop('user_id', None) + return redirect("/") diff --git a/docker-compose.yml b/docker-compose.yml index 18c247c..e62f6b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,18 @@ -version: '3.8' - -services: - wmgzon: - container_name: "wmgzon" - build: . - environment: - - APPSECRET=test - - ENVIRON=test - #- ENVIRON=prod - - FILESTORE=static/assets/img/products/ - tty: true - ports: - - "8080:8080" - volumes: - - .:/app - restart: unless-stopped +version: '3.8' + +services: + wmgzon: + container_name: "wmgzon" + build: . + environment: + - APPSECRET=test + - ENVIRON=test + #- ENVIRON=prod + - FILESTORE=static/assets/img/products/ + tty: true + ports: + - "8080:8080" + volumes: + - .:/app + restart: unless-stopped \ No newline at end of file diff --git a/dockerfile b/dockerfile index 2b9c37f..1025989 100644 --- a/dockerfile +++ b/dockerfile @@ -1,7 +1,7 @@ -FROM python:latest -COPY ./requirements.txt /app/requirements.txt -WORKDIR /app -RUN pip install -r requirements.txt -COPY . /app -RUN chmod +x scripts/run.bash +FROM python:latest +COPY ./requirements.txt /app/requirements.txt +WORKDIR /app +RUN pip install -r requirements.txt +COPY . /app +RUN chmod +x scripts/run.bash CMD ["bash", "scripts/run.bash"] \ No newline at end of file diff --git a/models/category.py b/models/category.py index 67e0820..39e6761 100644 --- a/models/category.py +++ b/models/category.py @@ -1,8 +1,8 @@ -class Category: - ''' - Constructor for a category object - ''' - - def __init__(self): - self.id = 0 - self.name = "" +class Category: + ''' + Constructor for a category object + ''' + + def __init__(self): + self.id = 0 + self.name = "" diff --git a/models/order.py b/models/order.py index d5b6486..51f3ae9 100644 --- a/models/order.py +++ b/models/order.py @@ -1,15 +1,15 @@ -from datetime import datetime - - -class Order: - ''' - Constructor for an order object - ''' - - def __init__(self): - self.id = 0 - self.sellerID = 0 - self.customerID = 0 - self.products = list() - self.totalCost = 0.0 - self.orderDate = datetime.now() +from datetime import datetime + + +class Order: + ''' + Constructor for an order object + ''' + + def __init__(self): + self.id = 0 + self.sellerID = 0 + self.customerID = 0 + self.products = list() + self.totalCost = 0.0 + self.orderDate = datetime.now() diff --git a/models/products/carpart.py b/models/products/carpart.py index 75b652a..2f1a1cf 100644 --- a/models/products/carpart.py +++ b/models/products/carpart.py @@ -1,14 +1,14 @@ -from product import Product - - -class CarPart(Product): - ''' - Constructor for a car part - - Contains additional information that is only relevant for car parts - ''' - - def __init__(self): - super().__init__() - self.make = "" - self.compatibleVehicles = list() +from product import Product + + +class CarPart(Product): + ''' + Constructor for a car part + + Contains additional information that is only relevant for car parts + ''' + + def __init__(self): + super().__init__() + self.make = "" + self.compatibleVehicles = list() diff --git a/models/products/product.py b/models/products/product.py index 57f9a66..19ef5dc 100644 --- a/models/products/product.py +++ b/models/products/product.py @@ -1,38 +1,38 @@ -from datetime import datetime - - -class Product: - ''' - Base class for a product - ''' - - def __init__(self): - self.id = 0 - self.name = "" - self.image = "/static/assets/wmgzon.png" - self.description = "" - self.cost = 0.0 - self.category = 0 - self.sellerID = 0 - self.postedDate = datetime.now() - self.quantityAvailable = 0 - - ''' - Class constructor to instatiate a customer object - - No additional properties are assigned to the customer - ''' - - def __init__(self, name: str, image: str, description: str, - cost: float, category: int, seller_id: int, - posted_date: datetime, quantity_available: int - ): - self.id = 0 - self.name = name - self.image = image - self.description = description - self.cost = cost - self.category = category - self.sellerID = seller_id - self.postedDate = posted_date - self.quantityAvailable = quantity_available +from datetime import datetime + + +class Product: + ''' + Base class for a product + ''' + + def __init__(self): + self.id = 0 + self.name = "" + self.image = "/static/assets/wmgzon.png" + self.description = "" + self.cost = 0.0 + self.category = 0 + self.sellerID = 0 + self.postedDate = datetime.now() + self.quantityAvailable = 0 + + ''' + Class constructor to instatiate a customer object + + No additional properties are assigned to the customer + ''' + + def __init__(self, name: str, image: str, description: str, + cost: float, category: int, seller_id: int, + posted_date: datetime, quantity_available: int + ): + self.id = 0 + self.name = name + self.image = image + self.description = description + self.cost = cost + self.category = category + self.sellerID = seller_id + self.postedDate = posted_date + self.quantityAvailable = quantity_available diff --git a/models/users/admin.py b/models/users/admin.py index 08a9715..4e40c41 100644 --- a/models/users/admin.py +++ b/models/users/admin.py @@ -1,15 +1,15 @@ -from .user import User - - -class Admin(User): - ''' - Class constructor to instatiate an admin object - - No additional properties are assigned to the admin - ''' - - def __init__(self, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str): - super().__init__( - username, password, firstname, lastname, email, phone, "Admin" - ) +from .user import User + + +class Admin(User): + ''' + Class constructor to instatiate an admin object + + No additional properties are assigned to the admin + ''' + + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): + super().__init__( + username, password, firstname, lastname, email, phone, "Admin" + ) diff --git a/models/users/customer.py b/models/users/customer.py index 786090d..ffc6bbf 100644 --- a/models/users/customer.py +++ b/models/users/customer.py @@ -1,15 +1,15 @@ -from .user import User - - -class Customer(User): - ''' - Class constructor to instatiate a customer object - - No additional properties are assigned to the customer - ''' - - def __init__(self, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str): - super().__init__( - username, password, firstname, lastname, email, phone, "Customer" - ) +from .user import User + + +class Customer(User): + ''' + Class constructor to instatiate a customer object + + No additional properties are assigned to the customer + ''' + + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): + super().__init__( + username, password, firstname, lastname, email, phone, "Customer" + ) diff --git a/models/users/seller.py b/models/users/seller.py index 5a3622a..5e75d4b 100644 --- a/models/users/seller.py +++ b/models/users/seller.py @@ -1,16 +1,16 @@ -from .user import User - - -class Seller(User): - ''' - Class constructor to instatiate a customer object - - No additional properties are assigned to the customer - ''' - - def __init__(self, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str): - super().__init__( - username, password, firstname, lastname, email, phone, "Seller" - ) - self.store = "" +from .user import User + + +class Seller(User): + ''' + Class constructor to instatiate a customer object + + No additional properties are assigned to the customer + ''' + + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): + super().__init__( + username, password, firstname, lastname, email, phone, "Seller" + ) + self.store = "" diff --git a/models/users/user.py b/models/users/user.py index 7a3e6f5..ced42aa 100644 --- a/models/users/user.py +++ b/models/users/user.py @@ -1,20 +1,20 @@ -from abc import ABC - - -class User(ABC): - """ Functional Class constructor to initialise all properties in - the base object with a value - """ - - def __init__(self, username: str, password: str, - firstname: str, lastname: str, - email: str, phone: str, role: str - ): - self.id = 0 - self.username = username - self.password = password - self.firstName = firstname - self.lastName = lastname - self.email = email - self.phone = phone - self.role = role +from abc import ABC + + +class User(ABC): + """ Functional Class constructor to initialise all properties in + the base object with a value + """ + + def __init__(self, username: str, password: str, + firstname: str, lastname: str, + email: str, phone: str, role: str + ): + self.id = 0 + self.username = username + self.password = password + self.firstName = firstname + self.lastName = lastname + self.email = email + self.phone = phone + self.role = role diff --git a/requirements.txt b/requirements.txt index bf7666b..7b8a215 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,26 @@ -blinker==1.7.0 -click==8.1.7 -colorama==0.4.6 -distlib==0.3.7 -dnspython==2.4.2 -docstring-to-markdown==0.13 -filelock==3.13.1 -Flask==3.0.0 -iniconfig==2.0.0 -itsdangerous==2.1.2 -jedi==0.19.1 -Jinja2==3.1.2 -MarkupSafe==2.1.3 -packaging==23.2 -parso==0.8.3 -platformdirs==3.11.0 -pluggy==1.3.0 -pymongo==4.6.0 -pytest==7.4.3 -python-lsp-jsonrpc==1.1.2 -python-lsp-server==1.9.0 -ujson==5.8.0 -virtualenv==20.24.6 -waitress==2.1.2 -Werkzeug==3.0.1 +blinker==1.7.0 +click==8.1.7 +colorama==0.4.6 +distlib==0.3.7 +dnspython==2.4.2 +docstring-to-markdown==0.13 +filelock==3.13.1 +Flask==3.0.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +jedi==0.19.1 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +packaging==23.2 +parso==0.8.3 +platformdirs==3.11.0 +pluggy==1.3.0 +pymongo==4.6.0 +pytest==7.4.3 +python-lsp-jsonrpc==1.1.2 +python-lsp-server==1.9.0 +ujson==5.8.0 +virtualenv==20.24.6 +waitress==2.1.2 +Werkzeug==3.0.1 pycodestyle==2.11.1 \ No newline at end of file diff --git a/scripts/create_database.py b/scripts/create_database.py index 209600f..3a841eb 100644 --- a/scripts/create_database.py +++ b/scripts/create_database.py @@ -1,51 +1,51 @@ -import sqlite3 -import os -import sys - -if __name__ == "__main__": - sys.path.append(os.getcwd()) - -from utils.file_utils import create_directory, remove_file - - -def create_connection(path: str, filename: str): - """ create a database connection to a SQLite database """ - conn = None - try: - # Make the directory for the file to go into - create_directory(path) - - print("Opening Database file and ensuring table integrity") - conn = sqlite3.connect(path + filename) - - print("Database file open") - # Execute creation scripts - sql = open("scripts/create_tables.sql", "r") - conn.executescript(sql.read()) - - print("Table creation complete") - - # Populate with test data if we are in Test Mode - if os.environ.get("ENVIRON") == "test": - sql = open("scripts/test_data.sql", "r") - conn.executescript(sql.read()) - - except sqlite3.Error as e: - print(e) - finally: - if conn: - conn.close() - - -# Ensure a directory is created given a path to it -dir = r"./data/" -db_name = r"wmgzon.db" - -# Check for test environ -if os.environ.get("ENVIRON") == "test": - # Remove the original test database - print("TEST ENVIRONMENT ACTIVE") - db_name = "test_" + db_name - remove_file(dir + db_name) - -create_connection(dir, db_name) +import sqlite3 +import os +import sys + +if __name__ == "__main__": + sys.path.append(os.getcwd()) + +from utils.file_utils import create_directory, remove_file + + +def create_connection(path: str, filename: str): + """ create a database connection to a SQLite database """ + conn = None + try: + # Make the directory for the file to go into + create_directory(path) + + print("Opening Database file and ensuring table integrity") + conn = sqlite3.connect(path + filename) + + print("Database file open") + # Execute creation scripts + sql = open("scripts/create_tables.sql", "r") + conn.executescript(sql.read()) + + print("Table creation complete") + + # Populate with test data if we are in Test Mode + if os.environ.get("ENVIRON") == "test": + sql = open("scripts/test_data.sql", "r") + conn.executescript(sql.read()) + + except sqlite3.Error as e: + print(e) + finally: + if conn: + conn.close() + + +# Ensure a directory is created given a path to it +dir = r"./data/" +db_name = r"wmgzon.db" + +# Check for test environ +if os.environ.get("ENVIRON") == "test": + # Remove the original test database + print("TEST ENVIRONMENT ACTIVE") + db_name = "test_" + db_name + remove_file(dir + db_name) + +create_connection(dir, db_name) diff --git a/scripts/create_tables.sql b/scripts/create_tables.sql index 5a582c2..5c587f7 100644 --- a/scripts/create_tables.sql +++ b/scripts/create_tables.sql @@ -1,56 +1,56 @@ -CREATE TABLE IF NOT EXISTS Users ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - password TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - email TEXT NOT NULL UNIQUE, - phone TEXT NOT NULL, - role TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS Categories ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL UNIQUE -); - -INSERT INTO Categories (name) VALUES ("Car Parts"); -INSERT INTO Categories (name) VALUES ("Animals"); -INSERT INTO Categories (name) VALUES ("Sports"); -INSERT INTO Categories (name) VALUES ("Books"); -INSERT INTO Categories (name) VALUES ("Phones"); -INSERT INTO Categories (name) VALUES ("Music"); - -CREATE TABLE IF NOT EXISTS Products ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - image TEXT NOT NULL, - description TEXT NOT NULL, - cost DECIMAL NOT NULL, - sellerID INTEGER NOT NULL - REFERENCES Users (id) - ON DELETE CASCADE - ON UPDATE NO ACTION, - categoryID INTEGER NOT NULL - REFERENCES Categories (id) - ON DELETE CASCADE - ON UPDATE NO ACTION, - postedDate TIMESTAMP, - quantityAvailable INTEGER DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS Orders ( - id INTEGER PRIMARY KEY, - sellerID TEXT NOT NULL - REFERENCES Users (id) - ON DELETE NO ACTION - ON UPDATE NO ACTION, - total DECIMAL NOT NULL, - buyerID INTEGER NOT NULL - REFERENCES Users (id) - ON DELETE CASCADE - ON UPDATE NO ACTION, - orderDate DATE NOT NULL -); - - +CREATE TABLE IF NOT EXISTS Users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + phone TEXT NOT NULL, + role TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS Categories ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +INSERT INTO Categories (name) VALUES ("Car Parts"); +INSERT INTO Categories (name) VALUES ("Animals"); +INSERT INTO Categories (name) VALUES ("Sports"); +INSERT INTO Categories (name) VALUES ("Books"); +INSERT INTO Categories (name) VALUES ("Phones"); +INSERT INTO Categories (name) VALUES ("Music"); + +CREATE TABLE IF NOT EXISTS Products ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + image TEXT NOT NULL, + description TEXT NOT NULL, + cost DECIMAL NOT NULL, + sellerID INTEGER NOT NULL + REFERENCES Users (id) + ON DELETE CASCADE + ON UPDATE NO ACTION, + categoryID INTEGER NOT NULL + REFERENCES Categories (id) + ON DELETE CASCADE + ON UPDATE NO ACTION, + postedDate TIMESTAMP, + quantityAvailable INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS Orders ( + id INTEGER PRIMARY KEY, + sellerID TEXT NOT NULL + REFERENCES Users (id) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + total DECIMAL NOT NULL, + buyerID INTEGER NOT NULL + REFERENCES Users (id) + ON DELETE CASCADE + ON UPDATE NO ACTION, + orderDate DATE NOT NULL +); + + diff --git a/scripts/test_data.sql b/scripts/test_data.sql index d64d349..d678033 100644 --- a/scripts/test_data.sql +++ b/scripts/test_data.sql @@ -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 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, 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, 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, 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, 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, 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, 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, 6); +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, 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, 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, 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, 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, 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, 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, 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); \ No newline at end of file diff --git a/static/css/alerts.css b/static/css/alerts.css index a547bf3..fa7432c 100644 --- a/static/css/alerts.css +++ b/static/css/alerts.css @@ -1,73 +1,73 @@ -.alert { - position: fixed; - bottom: 3em; - left: 50%; - transform: translate(-50%, 50%); - z-index: 1; - width: auto; - height: auto; - padding: 10px; - margin: 10px; - line-height: 1.8; - border-radius: 5px; - cursor: hand; - cursor: pointer; - font-family: sans-serif; - font-weight: 400; -} - -.alertCheckbox { - display: none; -} - -:checked + .alert { - display: none; -} - -.alertText { - display: table; - margin: 0 auto; - padding: 0 5px 0 0; - text-align: center; - font-size: 20px; -} - -.alertClose { - float: right; - padding-top: 5px; - font-size: 15px; -} - -.clear { - clear: both; -} - -.info { - background-color: #EEE; - border: 1px solid #DDD; - color: #999; -} - -.success { - background-color: #EFE; - border: 1px solid #DED; - color: #9A9; -} - -.notice { - background-color: #EFF; - border: 1px solid #DEE; - color: #9AA; -} - -.warning { - background-color: #FDF7DF; - border: 1px solid #FEEC6F; - color: #C9971C; -} - -.error { - background-color: #FEE; - border: 1px solid #EDD; - color: #A66; +.alert { + position: fixed; + bottom: 3em; + left: 50%; + transform: translate(-50%, 50%); + z-index: 1; + width: auto; + height: auto; + padding: 10px; + margin: 10px; + line-height: 1.8; + border-radius: 5px; + cursor: hand; + cursor: pointer; + font-family: sans-serif; + font-weight: 400; +} + +.alertCheckbox { + display: none; +} + +:checked + .alert { + display: none; +} + +.alertText { + display: table; + margin: 0 auto; + padding: 0 5px 0 0; + text-align: center; + font-size: 20px; +} + +.alertClose { + float: right; + padding-top: 5px; + font-size: 15px; +} + +.clear { + clear: both; +} + +.info { + background-color: #EEE; + border: 1px solid #DDD; + color: #999; +} + +.success { + background-color: #EFE; + border: 1px solid #DED; + color: #9A9; +} + +.notice { + background-color: #EFF; + border: 1px solid #DEE; + color: #9AA; +} + +.warning { + background-color: #FDF7DF; + border: 1px solid #FEEC6F; + color: #C9971C; +} + +.error { + background-color: #FEE; + border: 1px solid #EDD; + color: #A66; } \ No newline at end of file diff --git a/static/css/carparts.css b/static/css/carparts.css index b1cdf92..67c5a67 100644 --- a/static/css/carparts.css +++ b/static/css/carparts.css @@ -1,69 +1,69 @@ -.filter-pane { - flex: 0 1 auto; - width: 60%; - padding: .5rem 2rem; - background-color: var(--bg-grad-3); - border-radius: 1rem; -} - -.filter-items { - display: flex; - align-items: center; - justify-content: space-between; - gap: 2rem; -} - -.product-filter { - width: 100%; - padding: 16px 20px; - border: none; - border-radius: 4px; - background-color: var(--bg-grad-2); - color: var(--fg); - font-size: .75rem; -} - - - - -/* Number Plate*/ -.number-plate { - display: flex; - align-items: center; -} - -.country-identifier { - width: auto; - display: flex; - flex-direction: column; - align-items: center; - color: #ffffff; - font-weight: bold; - height: 60px; - justify-content: center; -} -.country-identifier img { - border-radius: 8px 0 0 8px; -} -.vrn { - width: 10rem; - height: 63px; - border: 1px solid #ead809; - background-color: #ead809; - border-radius: 0 8px 8px 0; -} -.vrn .vrn-text { - width: -webkit-fill-available; - height: -webkit-fill-available; - border-radius: 0; - border: 0; - font-family: "UKNumberPlate", sans-serif; - font-size: 180%; - text-transform: uppercase; - text-align: center; - outline: 0; - background-color: transparent; -} -.vrn .vrn-text:focus { - outline: 0; +.filter-pane { + flex: 0 1 auto; + width: 60%; + padding: .5rem 2rem; + background-color: var(--bg-grad-3); + border-radius: 1rem; +} + +.filter-items { + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.product-filter { + width: 100%; + padding: 16px 20px; + border: none; + border-radius: 4px; + background-color: var(--bg-grad-2); + color: var(--fg); + font-size: .75rem; +} + + + + +/* Number Plate*/ +.number-plate { + display: flex; + align-items: center; +} + +.country-identifier { + width: auto; + display: flex; + flex-direction: column; + align-items: center; + color: #ffffff; + font-weight: bold; + height: 60px; + justify-content: center; +} +.country-identifier img { + border-radius: 8px 0 0 8px; +} +.vrn { + width: 10rem; + height: 63px; + border: 1px solid #ead809; + background-color: #ead809; + border-radius: 0 8px 8px 0; +} +.vrn .vrn-text { + width: -webkit-fill-available; + height: -webkit-fill-available; + border-radius: 0; + border: 0; + font-family: "UKNumberPlate", sans-serif; + font-size: 180%; + text-transform: uppercase; + text-align: center; + outline: 0; + background-color: transparent; +} +.vrn .vrn-text:focus { + outline: 0; } \ No newline at end of file diff --git a/static/css/loginform.css b/static/css/loginform.css index cec1f1d..5d971a3 100644 --- a/static/css/loginform.css +++ b/static/css/loginform.css @@ -1,155 +1,155 @@ -h2 { - font-weight:300; - text-align:center; -} - -#input-form-wrap { - background-color: rgba(255, 255, 255, .15); - backdrop-filter: blur(200px); - width: 35%; - text-align: center; - padding: 1em 0 0 0; - border-radius: 4px; - box-shadow: 0px 30px 50px 0px rgba(0, 0, 0, 0.2); -} - -.input-form { - padding: 1em 2em; - display: flex; - flex-direction: column; - justify-items: center; - gap: 1em; -} - -.input-form-row { - display: flex; - flex-direction: row; - gap: 1em 1em; -} - -.input-form input, .input-form select, .input-form option, .input-form textarea { - width: 100%; - padding: 0 0 0 10px; - margin: 0; - color: var(--fg); - border: 1px solid var(--fg); - box-sizing: border-box; - outline: none; - height: 60px; - line-height: 60px; - border-radius: 4px; - font-style: normal; - font-size: 16px; - appearance: none; - position: relative; - display: inline-block; - background: none; - &:focus { - &:invalid { - color: var(--red); - border-color: var(--red); - } - } - - &:valid { - border-color: var(--green); - } -} - -.input-form textarea { - min-height: 120px; - max-width: 100%; -} - -.input-form input[type="submit"] { - border: none; - display:block; - background-color: rgba(255, 255, 255, .10); - color: var(--fg); - font-weight: bold; - text-transform:uppercase; - cursor: pointer; - transition: all 0.2s ease; - font-size: 18px; - position: relative; - display: inline-block; - cursor: pointer; - text-align: center; - &:hover { - background-color: rgba(255, 255, 255, .20); - transition: all 0.2s ease; - } -} -#create-account-wrap { - background-color: rgba(255, 255, 255, .15); - color:#dfdfdf; - font-size:14px; - width:100%; - padding:10px 0; - border-radius: 0 0 4px 4px; -} - -.checkbox { - display: block; - position: relative; - padding-left: 35px; - margin-bottom: 12px; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -/* Hide the browser's default checkbox */ -.checkbox input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; -} - -/* Create a custom checkbox */ -.checkmark { - position: absolute; - height: 25px; - width: 25px; - background-color: #808080; -} - -/* On mouse-over, add a grey background color */ -.checkbox:hover input ~ .checkmark { - background-color: #ccc; -} - -/* When the checkbox is checked, add a blue background */ -.checkbox input:checked ~ .checkmark { - background-color: #2196F3; -} - -/* Create the checkmark/indicator (hidden when not checked) */ -.checkmark:after { - content: ""; - position: absolute; - display: none; -} - -/* Show the checkmark when checked */ -.checkbox input:checked ~ .checkmark:after { - display: block; -} - -/* Style the checkmark/indicator */ -.checkbox .checkmark:after { - left: 9px; - top: 5px; - width: 5px; - height: 10px; - border: solid white; - border-width: 0 3px 3px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); +h2 { + font-weight:300; + text-align:center; +} + +#input-form-wrap { + background-color: rgba(255, 255, 255, .15); + backdrop-filter: blur(200px); + width: 35%; + text-align: center; + padding: 1em 0 0 0; + border-radius: 4px; + box-shadow: 0px 30px 50px 0px rgba(0, 0, 0, 0.2); +} + +.input-form { + padding: 1em 2em; + display: flex; + flex-direction: column; + justify-items: center; + gap: 1em; +} + +.input-form-row { + display: flex; + flex-direction: row; + gap: 1em 1em; +} + +.input-form input, .input-form select, .input-form option, .input-form textarea { + width: 100%; + padding: 0 0 0 10px; + margin: 0; + color: var(--fg); + border: 1px solid var(--fg); + box-sizing: border-box; + outline: none; + height: 60px; + line-height: 60px; + border-radius: 4px; + font-style: normal; + font-size: 16px; + appearance: none; + position: relative; + display: inline-block; + background: none; + &:focus { + &:invalid { + color: var(--red); + border-color: var(--red); + } + } + + &:valid { + border-color: var(--green); + } +} + +.input-form textarea { + min-height: 120px; + max-width: 100%; +} + +.input-form input[type="submit"] { + border: none; + display:block; + background-color: rgba(255, 255, 255, .10); + color: var(--fg); + font-weight: bold; + text-transform:uppercase; + cursor: pointer; + transition: all 0.2s ease; + font-size: 18px; + position: relative; + display: inline-block; + cursor: pointer; + text-align: center; + &:hover { + background-color: rgba(255, 255, 255, .20); + transition: all 0.2s ease; + } +} +#create-account-wrap { + background-color: rgba(255, 255, 255, .15); + color:#dfdfdf; + font-size:14px; + width:100%; + padding:10px 0; + border-radius: 0 0 4px 4px; +} + +.checkbox { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Hide the browser's default checkbox */ +.checkbox input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* Create a custom checkbox */ +.checkmark { + position: absolute; + height: 25px; + width: 25px; + background-color: #808080; +} + +/* On mouse-over, add a grey background color */ +.checkbox:hover input ~ .checkmark { + background-color: #ccc; +} + +/* When the checkbox is checked, add a blue background */ +.checkbox input:checked ~ .checkmark { + background-color: #2196F3; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.checkbox input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.checkbox .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); } \ No newline at end of file diff --git a/static/css/products.css b/static/css/products.css index b56f0bc..354e630 100644 --- a/static/css/products.css +++ b/static/css/products.css @@ -1,163 +1,163 @@ -/* Product Information*/ - -.product-link:after { - content: ''; - position: absolute; - bottom: 0; - left: 15%; - width: 0%; - border-bottom: 2px solid var(--fg); - transition: 0.4s; -} - -.product-link:hover:after { - width: 70%; - color: var(--hover); -} - -.product-link:hover { - color: var(--hover); -} - -.product-link:active { - color: var(--header); -} - -.product-container { - width: 95%; - background-color: var(--bg-grad-3); - border-radius: 1rem 1rem 0rem 0rem; - display: flex; - flex-wrap: wrap; - flex-direction: row; - gap: 2rem 1.5rem; - padding: 1rem; - transition: all 0.2s; - overflow-y: scroll; -} - -.product { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: 4 0 1rem; - max-width: 40%; - gap: .5em .5em; - padding: .5rem 0.2rem; - background: var(--bg-secondary); - border-radius: .5rem .5rem; - border: .1em solid rgba(255, 255, 255, 15%); - transition: all 0.2s; -} - -.product-fs { - height: 80%; - width: 80%; - font-size: 150%; - display: flex; - flex-direction: row; - justify-content: space-between; - gap: 2em 2em; - padding: .5rem 1rem 2rem 2rem; - /* background: var(--bg-secondary); */ - border-radius: .5rem .5rem; - transition: all 0.2s; -} - -.product-content-container { - display: flex; - flex-direction: row; - gap: 1em 1em; - padding: 0rem 1rem; -} - -.product-image-preview { - max-height: 150px; - max-width: 150px; -} - -.product-image { - max-height: 500px; - max-width: 500px; -} - -.product-title { - font-size: 120%; - font-weight: bold; -} - -.product-details { - display: flex; - flex-direction: column; - padding: 5% 0; - /* justify-content: center; */ - /* align-items: center; */ - gap: 1rem 1rem; -} - -.product-description { - font-size: 70%; -} - -.hide-overflow { - inline-size: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.product-acquisition-pane { - font-size: 80%; - width: 100rem; - max-width: 20%; - background-color: var(--bg-secondary); - border: .3em solid rgba(255, 255, 255, 15%); - border-radius: .5rem; - padding: 1rem 1rem; - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 1rem 1rem; -} - -.product-price { - font-weight: bold; -} - -.product-delivery { - font-size: 70%; -} - -.product-postage { - font-size: 70%; -} - -.product-stock { - display: flex; - flex-direction: row; - font-weight: bold; - justify-content: space-around; - flex-wrap: wrap; - align-items: center; -} -.product-quantity { - font-size: 50%; -} - -.product-instock { - color: var(--green); -} - -.product-nostock { - color: var(--red); -} - -.product-add-to-cart { - padding: .25rem .5rem; - margin-top: .25rem; - margin-right: .5rem; - font-size: 1rem; - border: none; - background-color: gray; +/* Product Information*/ + +.product-link:after { + content: ''; + position: absolute; + bottom: 0; + left: 15%; + width: 0%; + border-bottom: 2px solid var(--fg); + transition: 0.4s; +} + +.product-link:hover:after { + width: 70%; + color: var(--hover); +} + +.product-link:hover { + color: var(--hover); +} + +.product-link:active { + color: var(--header); +} + +.product-container { + width: 95%; + background-color: var(--bg-grad-3); + border-radius: 1rem 1rem 0rem 0rem; + display: flex; + flex-wrap: wrap; + flex-direction: row; + gap: 2rem 1.5rem; + padding: 1rem; + transition: all 0.2s; + overflow-y: scroll; +} + +.product { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 4 0 1rem; + max-width: 40%; + gap: .5em .5em; + padding: .5rem 0.2rem; + background: var(--bg-secondary); + border-radius: .5rem .5rem; + border: .1em solid rgba(255, 255, 255, 15%); + transition: all 0.2s; +} + +.product-fs { + height: 80%; + width: 80%; + font-size: 150%; + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 2em 2em; + padding: .5rem 1rem 2rem 2rem; + /* background: var(--bg-secondary); */ + border-radius: .5rem .5rem; + transition: all 0.2s; +} + +.product-content-container { + display: flex; + flex-direction: row; + gap: 1em 1em; + padding: 0rem 1rem; +} + +.product-image-preview { + max-height: 150px; + max-width: 150px; +} + +.product-image { + max-height: 500px; + max-width: 500px; +} + +.product-title { + font-size: 120%; + font-weight: bold; +} + +.product-details { + display: flex; + flex-direction: column; + padding: 5% 0; + /* justify-content: center; */ + /* align-items: center; */ + gap: 1rem 1rem; +} + +.product-description { + font-size: 70%; +} + +.hide-overflow { + inline-size: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.product-acquisition-pane { + font-size: 80%; + width: 100rem; + max-width: 20%; + background-color: var(--bg-secondary); + border: .3em solid rgba(255, 255, 255, 15%); + border-radius: .5rem; + padding: 1rem 1rem; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem 1rem; +} + +.product-price { + font-weight: bold; +} + +.product-delivery { + font-size: 70%; +} + +.product-postage { + font-size: 70%; +} + +.product-stock { + display: flex; + flex-direction: row; + font-weight: bold; + justify-content: space-around; + flex-wrap: wrap; + align-items: center; +} +.product-quantity { + font-size: 50%; +} + +.product-instock { + color: var(--green); +} + +.product-nostock { + color: var(--red); +} + +.product-add-to-cart { + padding: .25rem .5rem; + margin-top: .25rem; + margin-right: .5rem; + font-size: 1rem; + border: none; + background-color: gray; } \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index dc69804..4ae2f0a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,174 +1,174 @@ -:root { - --bg: #282c34; - --bg-secondary: #474d57; - --bg-grad-1: #484e58; - --bg-grad-2: #4e5560; - --bg-grad-3: #59616d; - --bg-grad-4: #606a7b; - --bg-grad-5: #606978; - --input: #4e5560; - --fg: #ABB2BF; - --header: #E06C75; - --link: #FFF; - --hover: #888; - - --green: #98C379; - --red: #E06C75; -} - -html { - background-color: var(--bg); - color: var(--fg); - font-size: 160%; - font-family: 'Courier New', Courier, 'Inter'; - height: 100%; -} - -body { - display: flex; - flex-direction: column; - gap: 1rem; - margin: 0; - height: 100%; -} - -.navbar { - flex: 0 1 auto; - padding: 30px; - display: flex; - flex-direction: column; - gap: 1rem; -} - -/* Navbar styling*/ -nav { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 120%; -} - -nav form { - display: flex; - justify-content: center; - align-items: center; -} - -.search-bar { - padding: .25rem; - margin-top: .25rem; - font-size: 1rem; - width: 15rem; - border: none; - transition: width 0.4s ease-in-out; - background-color: var(--fg); -} - -@media (min-width:1500px) { - .search-bar:focus { - width: 30rem; - } -} - -.search-button { - padding: .25rem .5rem; - margin-top: .25rem; - margin-right: .5rem; - background: #ddd; - font-size: 1rem; - border: none; - background-color: orange; -} - -.sticky { - position: -webkit-sticky; - position: sticky; - top: -2rem; -} - -#logo { - width: 8rem; - height: auto; -} - -*::-webkit-scrollbar, -*::-webkit-scrollbar-thumb { - width: 26px; - border-radius: 13px; - background-clip: padding-box; - border: 10px solid transparent; - color: var(--fg); -} -*::-webkit-scrollbar-thumb:hover{ - color: var(--link); -} - -*::-webkit-scrollbar-thumb { - box-shadow: inset 0 0 0 10px; -} - -@media (max-width:1000px) { - .not-required { - display: none; - } -} - -a { - text-decoration: none; - position: relative; - color: var(--link); - white-space: nowrap; - transition: 0.4s; - align-content: center; -} - -a:after { - content: ''; - position: absolute; - bottom: 0; - left: 15%; - width: 0%; - border-bottom: 2px solid var(--fg); - transition: 0.4s; -} - -a:hover:after { - width: 70%; - color: var(--hover); -} - -a:hover { - color: var(--hover); -} - -a:active { - color: var(--header); -} - -.categories { - background-color: var(--bg); - display: flex; - justify-content: center; -} - -.category { - margin: .5rem 0rem; - padding: 0rem 1rem; - border-left: .125rem solid var(--fg); -} - -.category:first-child { - border-left: none; -} - -.container { - flex: 1 1 auto; - display: flex; - justify-content: flex-start; - align-items: center; - width: 100%; - gap: 1rem; - flex-direction: column; - flex-grow: 1; - height: 1%; -} +:root { + --bg: #282c34; + --bg-secondary: #474d57; + --bg-grad-1: #484e58; + --bg-grad-2: #4e5560; + --bg-grad-3: #59616d; + --bg-grad-4: #606a7b; + --bg-grad-5: #606978; + --input: #4e5560; + --fg: #ABB2BF; + --header: #E06C75; + --link: #FFF; + --hover: #888; + + --green: #98C379; + --red: #E06C75; +} + +html { + background-color: var(--bg); + color: var(--fg); + font-size: 160%; + font-family: 'Courier New', Courier, 'Inter'; + height: 100%; +} + +body { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 0; + height: 100%; +} + +.navbar { + flex: 0 1 auto; + padding: 30px; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Navbar styling*/ +nav { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 120%; +} + +nav form { + display: flex; + justify-content: center; + align-items: center; +} + +.search-bar { + padding: .25rem; + margin-top: .25rem; + font-size: 1rem; + width: 15rem; + border: none; + transition: width 0.4s ease-in-out; + background-color: var(--fg); +} + +@media (min-width:1500px) { + .search-bar:focus { + width: 30rem; + } +} + +.search-button { + padding: .25rem .5rem; + margin-top: .25rem; + margin-right: .5rem; + background: #ddd; + font-size: 1rem; + border: none; + background-color: orange; +} + +.sticky { + position: -webkit-sticky; + position: sticky; + top: -2rem; +} + +#logo { + width: 8rem; + height: auto; +} + +*::-webkit-scrollbar, +*::-webkit-scrollbar-thumb { + width: 26px; + border-radius: 13px; + background-clip: padding-box; + border: 10px solid transparent; + color: var(--fg); +} +*::-webkit-scrollbar-thumb:hover{ + color: var(--link); +} + +*::-webkit-scrollbar-thumb { + box-shadow: inset 0 0 0 10px; +} + +@media (max-width:1000px) { + .not-required { + display: none; + } +} + +a { + text-decoration: none; + position: relative; + color: var(--link); + white-space: nowrap; + transition: 0.4s; + align-content: center; +} + +a:after { + content: ''; + position: absolute; + bottom: 0; + left: 15%; + width: 0%; + border-bottom: 2px solid var(--fg); + transition: 0.4s; +} + +a:hover:after { + width: 70%; + color: var(--hover); +} + +a:hover { + color: var(--hover); +} + +a:active { + color: var(--header); +} + +.categories { + background-color: var(--bg); + display: flex; + justify-content: center; +} + +.category { + margin: .5rem 0rem; + padding: 0rem 1rem; + border-left: .125rem solid var(--fg); +} + +.category:first-child { + border-left: none; +} + +.container { + flex: 1 1 auto; + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; + gap: 1rem; + flex-direction: column; + flex-grow: 1; + height: 1%; +} diff --git a/templates/Car Parts.html b/templates/Car Parts.html index 086f822..26bf504 100644 --- a/templates/Car Parts.html +++ b/templates/Car Parts.html @@ -1,22 +1,22 @@ - - -
-
-
- - - - - - -
- - - - -
-
+ + +
+
+
+ + + + + + +
+ + + + +
+
diff --git a/templates/content.html b/templates/content.html index 89b7646..d6a5525 100644 --- a/templates/content.html +++ b/templates/content.html @@ -1,20 +1,20 @@ - - -
- {% if products != None %} - {% for product in products %} - -
{{product.name}}
-
- Brake Disks - -
-
£{{product.cost}}
-
{{product.description}}
-
-
- -
- {% endfor %} - {% endif %} -
+ + +
+ {% if products != None %} + {% for product in products %} + +
{{product.name}}
+
+ Brake Disks + +
+
£{{product.cost}}
+
{{product.description}}
+
+
+ +
+ {% endfor %} + {% endif %} +
diff --git a/templates/header.html b/templates/header.html index d6b0407..b15e51e 100644 --- a/templates/header.html +++ b/templates/header.html @@ -1,51 +1,51 @@ - - -{% with messages = get_flashed_messages(with_categories=true)%} - {% if messages %} - {% for category, message in messages %} - - {% endfor%} - {% endif %} -{% endwith %} + + +{% with messages = get_flashed_messages(with_categories=true)%} + {% if messages %} + {% for category, message in messages %} + + {% endfor%} + {% endif %} +{% endwith %} diff --git a/templates/index.html b/templates/index.html index 6c8d582..34c25c7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,28 +1,28 @@ - - - - - - - - - - - - - WMGZON - - - {% include 'header.html' %} - - -
- {% if category is defined %} - {% set include_file = category+".html" %} - {% include include_file ignore missing %} - {% endif %} - - {% include content %} -
- + + + + + + + + + + + + + WMGZON + + + {% include 'header.html' %} + + +
+ {% if category is defined %} + {% set include_file = category+".html" %} + {% include include_file ignore missing %} + {% endif %} + + {% include content %} +
+ \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index 438b5be..a965234 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,14 +1,14 @@ - - -
-

Login

-
- - - -
- -
-

Not a member? Create Account

-

-
+ + +
+

Login

+
+ + + +
+ +
+

Not a member? Create Account

+

+
diff --git a/templates/new_product.html b/templates/new_product.html index d126d7b..8e7d1ad 100644 --- a/templates/new_product.html +++ b/templates/new_product.html @@ -1,29 +1,29 @@ - - -
-

Create New Product

-
-
- - -
- - - - - -
- - -
- -
- -
-

Want to view all of your products? Click Here

-

-
+ + +
+

Create New Product

+
+
+ + +
+ + + + + +
+ + +
+ +
+ +
+

Want to view all of your products? Click Here

+

+
diff --git a/templates/product.html b/templates/product.html index d628bd4..4df5929 100644 --- a/templates/product.html +++ b/templates/product.html @@ -1,65 +1,65 @@ - - - -
- {% if product != None %} - {% if user.id == product.sellerID %} -
-
- -
-
-
- Brake Disks - -
-
- -
- - -
- -
- -
- -
- -
-
-
- -
-
- {% else %} - Brake Disks -
-
{{product.name}}
-
{{product.description}}
-
-
-
£{{product.cost}}
-
Earliest Delivery Friday 24th December
-
+£{{product.cost}} P&P
-
- {% if product.quantityAvailable > 0 %} -
In Stock
-
{{product.quantityAvailable}} Available
- - {% else %} -
Out of Stock
- {% endif %} -
-
- {% endif %} - {% endif %} -
+ + + +
+ {% if product != None %} + {% if user.id == product.sellerID %} +
+
+ +
+
+
+ Brake Disks + +
+
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+ +
+
+ {% else %} + Brake Disks +
+
{{product.name}}
+
{{product.description}}
+
+
+
£{{product.cost}}
+
Earliest Delivery Friday 24th December
+
+£{{product.cost}} P&P
+
+ {% if product.quantityAvailable > 0 %} +
In Stock
+
{{product.quantityAvailable}} Available
+ + {% else %} +
Out of Stock
+ {% endif %} +
+
+ {% endif %} + {% endif %} +
diff --git a/templates/signup.html b/templates/signup.html index f4aeb67..b9112dc 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -1,27 +1,27 @@ - - -
-

Sign Up

-
-
- - -
- -
- - -
- - - - -
- -
-

Already have an account? Login

-

-
+ + +
+

Sign Up

+
+
+ + +
+ +
+ + +
+ + + + +
+ +
+

Already have an account? Login

+

+
diff --git a/tests/__init__.py b/tests/__init__.py index d1bee94..e8b9fa2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -import os - -# Setup test environment variables -os.environ["ENVIRON"] = "test" +import os + +# Setup test environment variables +os.environ["ENVIRON"] = "test" diff --git a/tests/database/__init__.py b/tests/database/__init__.py index 9bd5abe..200380e 100644 --- a/tests/database/__init__.py +++ b/tests/database/__init__.py @@ -1,3 +1,3 @@ -# Ensure test environment is set before using -# Runs the database creation scripts -import scripts.create_database +# Ensure test environment is set before using +# Runs the database creation scripts +import scripts.create_database diff --git a/tests/database/test_products.py b/tests/database/test_products.py index f5e2cc8..7073845 100644 --- a/tests/database/test_products.py +++ b/tests/database/test_products.py @@ -1,70 +1,70 @@ -import pytest -import sqlite3 -from datetime import datetime -from controllers.database.product import ProductController -from models.products.product import Product - -product = Product( - "product", - "image.png", - "description", - 10.00, - 1, - 1, - datetime.now(), - 1 -) - -# Tests a new product can be created - - -def test_create_product(): - db = ProductController() - db.create(product) - -# Tests the database maintains integrity when we try -# and add a product with the same details - - -@pytest.mark.skip -def test_duplicate_product(): - db = ProductController() - with pytest.raises(sqlite3.IntegrityError): - db.create(product) - -# Tests that products can be refined by category - - -def test_search_category(): - db = ProductController() - - # 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("Books")) == 9 - assert db.read_all("Phones") is None - - -# Tests that products can be refined by search term -def test_search_term(): - db = ProductController() - - # Check each search term for correct amount of test products - assert len(db.read_all(search_term="test")) == 33 - assert len(db.read_all("Car Parts", "test")) == 9 - assert len(db.read_all(search_term="product")) == 1 - assert db.read_all(search_term="not_test") is None - -# Test we the same product details get returned from the database - - -def test_read_product(): - db = ProductController() - - # Test the same product is returned - new_product = db.read("product") - assert isinstance(new_product, list) - assert isinstance(new_product[0], Product) - - # Update the ID on the item as database assigns new id - product.id = new_product[0].id - assert new_product[0].__dict__ == product.__dict__ +import pytest +import sqlite3 +from datetime import datetime +from controllers.database.product import ProductController +from models.products.product import Product + +product = Product( + "product", + "image.png", + "description", + 10.00, + 1, + 1, + datetime.now(), + 1 +) + +# Tests a new product can be created + + +def test_create_product(): + db = ProductController() + db.create(product) + +# Tests the database maintains integrity when we try +# and add a product with the same details + + +@pytest.mark.skip +def test_duplicate_product(): + db = ProductController() + with pytest.raises(sqlite3.IntegrityError): + db.create(product) + +# Tests that products can be refined by category + + +def test_search_category(): + db = ProductController() + + # 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("Books")) == 9 + assert db.read_all("Phones") is None + + +# Tests that products can be refined by search term +def test_search_term(): + db = ProductController() + + # Check each search term for correct amount of test products + assert len(db.read_all(search_term="test")) == 33 + assert len(db.read_all("Car Parts", "test")) == 9 + assert len(db.read_all(search_term="product")) == 1 + assert db.read_all(search_term="not_test") is None + +# Test we the same product details get returned from the database + + +def test_read_product(): + db = ProductController() + + # Test the same product is returned + new_product = db.read("product") + assert isinstance(new_product, list) + assert isinstance(new_product[0], Product) + + # Update the ID on the item as database assigns new id + product.id = new_product[0].id + assert new_product[0].__dict__ == product.__dict__ diff --git a/tests/database/test_users.py b/tests/database/test_users.py index 1f50c8a..0e82c3b 100644 --- a/tests/database/test_users.py +++ b/tests/database/test_users.py @@ -1,74 +1,74 @@ -import pytest -import sqlite3 -from controllers.database.user import UserController -from models.users.customer import Customer -from models.users.seller import Seller - -customer = Customer( - "testcustomer", - "Password1", - "firstname", - "lastname", - "test@test", - "123456789" -) - -seller = Seller( - "testseller", - "Password1", - "firstname", - "lastname", - "seller@seller", - "987654321" -) - -# Tests a new user can be created - - -def test_create_user(): - db = UserController() - db.create(customer) - -# Tests the database maintains integrity when we try -# and add a user with the same details - - -def test_duplicate_user(): - db = UserController() - with pytest.raises(sqlite3.IntegrityError): - db.create(customer) - -# Test we the same user details get returned from the database - - -def test_read_user(): - db = UserController() - - # Test the same user is returned - user = db.read(customer.username) - assert isinstance(user, Customer) - - # Update the ID on the item as database assigns new id - customer.id = user.id - assert user.__dict__ == customer.__dict__ - - -# Tests a new seller can be created -def test_create_seller(): - db = UserController() - db.create(seller) - -# Test that the same seller details get returned from the database - - -def test_read_seller(): - db = UserController() - - # Test the same user is returned - user = db.read(seller.username) - assert isinstance(user, Seller) - - # Update the ID on the item as database assigns new id - seller.id = user.id - user.store = "" - assert user.__dict__ == seller.__dict__ +import pytest +import sqlite3 +from controllers.database.user import UserController +from models.users.customer import Customer +from models.users.seller import Seller + +customer = Customer( + "testcustomer", + "Password1", + "firstname", + "lastname", + "test@test", + "123456789" +) + +seller = Seller( + "testseller", + "Password1", + "firstname", + "lastname", + "seller@seller", + "987654321" +) + +# Tests a new user can be created + + +def test_create_user(): + db = UserController() + db.create(customer) + +# Tests the database maintains integrity when we try +# and add a user with the same details + + +def test_duplicate_user(): + db = UserController() + with pytest.raises(sqlite3.IntegrityError): + db.create(customer) + +# Test we the same user details get returned from the database + + +def test_read_user(): + db = UserController() + + # Test the same user is returned + user = db.read(customer.username) + assert isinstance(user, Customer) + + # Update the ID on the item as database assigns new id + customer.id = user.id + assert user.__dict__ == customer.__dict__ + + +# Tests a new seller can be created +def test_create_seller(): + db = UserController() + db.create(seller) + +# Test that the same seller details get returned from the database + + +def test_read_seller(): + db = UserController() + + # Test the same user is returned + user = db.read(seller.username) + assert isinstance(user, Seller) + + # Update the ID on the item as database assigns new id + seller.id = user.id + user.store = "" + assert user.__dict__ == seller.__dict__ diff --git a/tests/general/test_pep8.py b/tests/general/test_pep8.py index 38cc521..3a17a92 100644 --- a/tests/general/test_pep8.py +++ b/tests/general/test_pep8.py @@ -1,11 +1,11 @@ -import pycodestyle - -# Tests files to ensure they conform to pep8 standards - - -def test_pep8_conformance(): - """Test that we conform to PEP8.""" - pep8style = pycodestyle.StyleGuide() - dirs = ["./controllers", "./models", "./scripts", "./tests", "./utils"] - result = pep8style.check_files(dirs) - assert result.total_errors == 0 +import pycodestyle + +# Tests files to ensure they conform to pep8 standards + + +def test_pep8_conformance(): + """Test that we conform to PEP8.""" + pep8style = pycodestyle.StyleGuide() + dirs = ["./controllers", "./models", "./scripts", "./tests", "./utils"] + result = pep8style.check_files(dirs) + assert result.total_errors == 0 diff --git a/utils/file_utils.py b/utils/file_utils.py index cf5c3a8..d124d6f 100644 --- a/utils/file_utils.py +++ b/utils/file_utils.py @@ -1,46 +1,46 @@ -import os -import uuid -import pathlib - -ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} - - -def allowed_file(filename) -> bool: - """ Ensures only filenames ending with the correct extension are allowed. - Note: This does not verify that the content inside of the file - matches the type specified - """ - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - - -def save_image(file) -> str | None: - """ Saves a given file to disk with a random UUID4 generated - filename. Returns the filename as a string. - """ - # Ensure that the correct file type is uploaded - if file is None or not allowed_file(file.filename): - return None - - # Create the product object and push to database - filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix - - path = os.environ.get('FILESTORE') - file.save(os.path.join(path, filename)) - return filename - - -def create_directory(dir: str): - """ Creates the given directory string is not alreay made """ - try: - os.makedirs(dir) - except FileExistsError: - pass - - -def remove_file(dir: str): - """ Removes a given file if it is present at the given dir """ - try: - os.remove(dir) - except FileNotFoundError: - pass +import os +import uuid +import pathlib + +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} + + +def allowed_file(filename) -> bool: + """ Ensures only filenames ending with the correct extension are allowed. + Note: This does not verify that the content inside of the file + matches the type specified + """ + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + +def save_image(file) -> str | None: + """ Saves a given file to disk with a random UUID4 generated + filename. Returns the filename as a string. + """ + # Ensure that the correct file type is uploaded + if file is None or not allowed_file(file.filename): + return None + + # Create the product object and push to database + filename = str(uuid.uuid4()) + pathlib.Path(file.filename).suffix + + path = os.environ.get('FILESTORE') + file.save(os.path.join(path, filename)) + return filename + + +def create_directory(dir: str): + """ Creates the given directory string is not alreay made """ + try: + os.makedirs(dir) + except FileExistsError: + pass + + +def remove_file(dir: str): + """ Removes a given file if it is present at the given dir """ + try: + os.remove(dir) + except FileNotFoundError: + pass diff --git a/utils/user_utils.py b/utils/user_utils.py index 6a5f973..310949c 100644 --- a/utils/user_utils.py +++ b/utils/user_utils.py @@ -1,26 +1,26 @@ -from flask import session -from models.users.user import User -from controllers.database.user import UserController - - -def is_logged_in() -> User | None: - """ Returns the user object if the user is logged in - Otherwise returns a None type - """ - user_id = session.get('user_id') - - if user_id is not None: - db = UserController() - return db.read_id(user_id) - return None - - -def is_role(role: str) -> bool: - """ Function that returns true if the user is logged in as""" - user = is_logged_in() - - if user is not None: - return user.role == role - - # User isn't logged in - return False +from flask import session +from models.users.user import User +from controllers.database.user import UserController + + +def is_logged_in() -> User | None: + """ Returns the user object if the user is logged in + Otherwise returns a None type + """ + user_id = session.get('user_id') + + if user_id is not None: + db = UserController() + return db.read_id(user_id) + return None + + +def is_role(role: str) -> bool: + """ Function that returns true if the user is logged in as""" + user = is_logged_in() + + if user is not None: + return user.role == role + + # User isn't logged in + return False