Initial commit

This commit is contained in:
Benoy Bose 2024-10-06 18:06:34 +00:00
commit ab0b4cfef1
15 changed files with 470 additions and 0 deletions

50
.devcontainer/devcontainer.json Executable file
View File

@ -0,0 +1,50 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
{
"name": "Existing Docker Compose (Extend)",
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],
// The 'service' property is the name of the service for the container that VS Code should
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
"service": "app",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",
// Uncomment the next line to run commands after the container is created.
// "postCreateCommand": "cat /etc/os-release",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"codeium.codeium",
"ms-azuretools.vscode-docker"
]
}
}
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "devcontainer"
}

View File

@ -0,0 +1,26 @@
version: '3.8'
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
app:
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
# array). The sample below assumes your primary file is in the root of your project.
#
# build:
# context: .
# dockerfile: .devcontainer/Dockerfile
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity

1
.dockerignore Executable file
View File

@ -0,0 +1 @@
data/

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
__pycache__/
**/__pycache__/
data/

25
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "app.py",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger",
"--no-reload"
],
"jinja": true,
"autoStartBrowser": false
}
]
}

6
Dockerfile Executable file
View File

@ -0,0 +1,6 @@
FROM python:3.12-bookworm
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 80
CMD ["python", "app.py"]

15
app.py Executable file
View File

@ -0,0 +1,15 @@
from flask import Flask
import os
def create_app():
app = Flask(__name__)
if "SQLALCHEMY_DATABASE_URI" in os.environ.keys():
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['SQLALCHEMY_DATABASE_URI']
from models import db
db.init_app(app)
from flask_migrate import Migrate
migrate = Migrate(app, db)
from flask_migrate import upgrade
with app.app_context():
upgrade(directory="migrations")
return app

21
docker-compose.yml Executable file
View File

@ -0,0 +1,21 @@
services:
app:
container_name: staffportal-app
build: .
ports:
- "8000:80"
depends_on:
- db
environment:
- SQLALCHEMY_DATABASE_URI=mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@${MYSQL_HOST}:3306/${MYSQL_DATABASE}
db:
image: mysql:9
container_name: staffportal-db
environment:
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
volumes:
- ./data:/var/lib/mysql

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Single-database configuration for Flask.

50
migrations/alembic.ini Normal file
View File

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

113
migrations/env.py Normal file
View File

@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,72 @@
"""initial migration
Revision ID: 936d19bcafbf
Revises:
Create Date: 2024-10-06 17:53:11.262950
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '936d19bcafbf'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('roles',
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('createdAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updatedAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('staff',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('firstName', sa.String(length=40), nullable=False),
sa.Column('lastName', sa.String(length=40), nullable=False),
sa.Column('uid', sa.String(length=100), nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('password', sa.String(length=200), nullable=True),
sa.Column('dateOfBirth', sa.DateTime(timezone=True), nullable=True),
sa.Column('roleId', sa.Integer(), nullable=False),
sa.Column('revoked', sa.Boolean(), nullable=False),
sa.Column('createdAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updatedAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['roleId'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('uid')
)
with op.batch_alter_table('staff', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_staff_firstName'), ['firstName'], unique=False)
batch_op.create_index(batch_op.f('ix_staff_lastName'), ['lastName'], unique=False)
op.create_table('email_aliases',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('staffId', sa.Integer(), nullable=False),
sa.Column('createdAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updatedAt', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['staffId'], ['staff.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('email_aliases')
with op.batch_alter_table('staff', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_staff_lastName'))
batch_op.drop_index(batch_op.f('ix_staff_firstName'))
op.drop_table('staff')
op.drop_table('roles')
# ### end Alembic commands ###

43
models.py Normal file
View File

@ -0,0 +1,43 @@
from typing import List
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey, types, func
from datetime import datetime
db = SQLAlchemy()
class RoleModel(db.Model):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False)
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
createdAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now())
updatedAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
staff: Mapped[List["StaffModel"]] = relationship(back_populates="role")
class StaffModel(db.Model):
__tablename__ = "staff"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
firstName: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
lastName: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
uid: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(200), nullable=True)
dateOfBirth: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=True)
roleId: Mapped[int] = mapped_column(ForeignKey("roles.id"))
revoked: Mapped[bool] = mapped_column(default=False)
createdAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now())
updatedAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
role: Mapped["RoleModel"] = relationship(back_populates="staff")
email_aliases: Mapped[List["EmailAliasModel"]] = relationship(back_populates="staff")
class EmailAliasModel(db.Model):
__tablename__ = "email_aliases"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
staffId: Mapped[int] = mapped_column(ForeignKey("staff.id"))
createdAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now())
updatedAt: Mapped[datetime] = mapped_column(types.DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
staff: Mapped["StaffModel"] = relationship(back_populates="email_aliases")

19
requirements.txt Executable file
View File

@ -0,0 +1,19 @@
alembic==1.13.3
Authlib==1.3.2
blinker==1.8.2
cffi==1.17.1
click==8.1.7
cryptography==43.0.1
Flask==3.0.3
Flask-Migrate==4.0.7
Flask-SQLAlchemy==3.1.1
greenlet==3.1.1
itsdangerous==2.2.0
Jinja2==3.1.4
Mako==1.3.5
MarkupSafe==2.1.5
pycparser==2.22
SQLAlchemy==2.0.35
typing_extensions==4.12.2
Werkzeug==3.0.4
PyMySQL==1.1.1