Skip to content

Commit

Permalink
Merge pull request #3 from neilshaabi/1.2_unit-test
Browse files Browse the repository at this point in the history
1.2 Unit tests for auth + flake8 linting
  • Loading branch information
neilshaabi authored Feb 25, 2024
2 parents 936309e + 9b18be7 commit 6323ce4
Show file tree
Hide file tree
Showing 21 changed files with 979 additions and 221 deletions.
6 changes: 6 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[flake8]
ignore = E501, W503
max-line-length = 88
exclude =
.venv,
migrations
27 changes: 21 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
venv:
@echo "\nCreating virtual environment..."
@echo "Creating virtual environment..."
python3 -m venv .venv
@echo "\nNote: You will need to activate the virtual environment in your shell manually using:"
@echo "Note: You will need to activate the virtual environment in your shell manually using:"
@echo "source .venv/bin/activate"

deps:
@echo "\nInstalling dependencies..."
@echo "Installing dependencies..."
pip install -r requirements.txt

app:
@echo "\nRunning Flask app locally..."
@echo "Running Flask app locally..."
flask run

clean:
@echo "\nCleaning up directory..."
@echo "Cleaning up directory..."
rm -rf .venv
find . -type d -name '__pycache__' -exec rm -r {} +
find . -type f -name '*.pyc' -delete

.PHONY: venv deps app clean
lint:
@echo "Reorganising imports..."
isort .
@echo "Formatting Python files..."
black . --exclude '/(\.venv|migrations)/'
@echo "Linting Python files..."
flake8 --exclude .venv,./migrations

test:
@echo "Running tests with pytest..."
pytest -s

help:
@echo "Available commands: make [help, venv, deps, app, test, clean]"

.PHONY: help venv deps app lint test clean
19 changes: 9 additions & 10 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import os

from flask import Flask
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from itsdangerous import URLSafeTimedSerializer

from app.config import Config, selected_config
from app.config import CONFIGS, Config

db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()
mail = Mail()


login_manager = LoginManager()
login_manager.login_view = "/"
login_manager.login_message = None

selected_config = CONFIGS[os.environ["ENV"]]


def create_app(config: Config = selected_config):
app = Flask(__name__)
app.config.from_object(config)

# Initialise extensions
db.init_app(app)
migrate.init_app(app, db)
mail.init_app(app)
login_manager.init_app(app)
app.serialiser = URLSafeTimedSerializer(app.config["SECRET_KEY"])

# Reset database
from app.models import User, insertDummyData

if app.config["RESET_DB"]:
with app.app_context():
db.drop_all()
db.create_all()
insertDummyData()

# Register blueprints
from app.views import auth, main

Expand Down
5 changes: 0 additions & 5 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,16 @@ class Config(object):

class DevConfig(Config):
DEBUG: bool = True
RESET_DB: bool = True
SQLALCHEMY_DATABASE_URI: str = "sqlite:///" + os.path.join(basedir, "mindli.sqlite")


class ProdConfig(Config):
DEBUG: bool = False
RESET_DB: bool = False
SQLALCHEMY_DATABASE_URI: str = os.environ["DATABASE_URL"]


class TestConfig(Config):
TESTING: bool = True
RESET_DB: bool = False
SQLALCHEMY_DATABASE_URI: str = "sqlite://" # Use in-memory database


Expand All @@ -45,5 +42,3 @@ class TestConfig(Config):
"prod": ProdConfig,
"test": TestConfig,
}

selected_config = CONFIGS[os.environ["ENV"]]
127 changes: 82 additions & 45 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,29 @@ class User(UserMixin, db.Model):
active: so.Mapped[bool] = so.mapped_column(sa.Boolean, default=True)
gender: so.Mapped[Optional["Gender"]] = so.mapped_column(sa.Enum(Gender))
photo_url: so.Mapped[Optional[str]] = so.mapped_column(sa.String(255))
timezone: so.Mapped[Optional[str]] = so.mapped_column(sa.String(50)) # IANA Time Zone Database name
currency: so.Mapped[Optional[str]] = so.mapped_column(sa.String(3)) # ISO 4217 currency code

timezone: so.Mapped[Optional[str]] = so.mapped_column(
sa.String(50)
) # IANA Time Zone Database name
currency: so.Mapped[Optional[str]] = so.mapped_column(
sa.String(3)
) # ISO 4217 currency code

client: so.Mapped[Optional["Client"]] = so.relationship(back_populates="user")
therapist: so.Mapped[Optional["Therapist"]] = so.relationship(back_populates="user")


class Client(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey('user.id'), index=True)
user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("user.id"), index=True)
preferred_gender: so.Mapped[Optional["Gender"]] = so.mapped_column(sa.Enum(Gender))
preferred_language_id: so.Mapped[Optional[int]] = so.mapped_column(sa.ForeignKey('language.id'))

preferred_language_id: so.Mapped[Optional[int]] = so.mapped_column(
sa.ForeignKey("language.id")
)

user: so.Mapped["User"] = so.relationship(back_populates="client")
issues: so.Mapped[List["Issue"]] = so.relationship(secondary=client_issue, back_populates="clients")
issues: so.Mapped[List["Issue"]] = so.relationship(
secondary=client_issue, back_populates="clients"
)
preferred_language: so.Mapped[Optional["Language"]] = so.relationship("Language")


Expand All @@ -112,87 +120,116 @@ class Therapist(db.Model):
registrations: so.Mapped[Optional[str]] = so.mapped_column(sa.Text)
qualifications: so.Mapped[Optional[str]] = so.mapped_column(sa.Text)
years_of_experience: so.Mapped[Optional[int]] = so.mapped_column(sa.Integer)

user: so.Mapped["User"] = so.relationship(back_populates="therapist")
languages: so.Mapped[List["Language"]] = so.relationship(secondary=therapist_language, back_populates="therapists")
specialisations: so.Mapped[List["Issue"]] = so.relationship(secondary=therapist_issue, back_populates="therapists")
interventions: so.Mapped[List["Intervention"]] = so.relationship(secondary=therapist_intervention, back_populates="therapists")
session_types: so.Mapped[List["SessionType"]] = so.relationship(back_populates="therapist")
availabilities: so.Mapped[List["Availability"]] = so.relationship(back_populates="therapist")
unavailabilities: so.Mapped[List["Unavailability"]] = so.relationship(back_populates="therapist")
languages: so.Mapped[List["Language"]] = so.relationship(
secondary=therapist_language, back_populates="therapists"
)
specialisations: so.Mapped[List["Issue"]] = so.relationship(
secondary=therapist_issue, back_populates="therapists"
)
interventions: so.Mapped[List["Intervention"]] = so.relationship(
secondary=therapist_intervention, back_populates="therapists"
)
session_types: so.Mapped[List["SessionType"]] = so.relationship(
back_populates="therapist"
)
availabilities: so.Mapped[List["Availability"]] = so.relationship(
back_populates="therapist"
)
unavailabilities: so.Mapped[List["Unavailability"]] = so.relationship(
back_populates="therapist"
)


class Language(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
name: so.Mapped[str] = so.mapped_column(sa.String(50), unique=True)
iso639_1: so.Mapped[Optional[str]] = so.mapped_column(sa.String(2), unique=True) # ISO 639-1 two-letter code
iso639_2: so.Mapped[Optional[str]] = so.mapped_column(sa.String(3), unique=True) # ISO 639-2 three-letter code

therapists: so.Mapped[List["Therapist"]] = so.relationship(secondary=therapist_language, back_populates="languages")
iso639_1: so.Mapped[Optional[str]] = so.mapped_column(
sa.String(2), unique=True
) # ISO 639-1 two-letter code
iso639_2: so.Mapped[Optional[str]] = so.mapped_column(
sa.String(3), unique=True
) # ISO 639-2 three-letter code

therapists: so.Mapped[List["Therapist"]] = so.relationship(
secondary=therapist_language, back_populates="languages"
)


class Issue(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
name: so.Mapped[str] = so.mapped_column(sa.String(50), unique=True)

clients: so.Mapped[List["Client"]] = so.relationship(secondary=client_issue, back_populates="issues")
therapists: so.Mapped[List["Therapist"]] = so.relationship(secondary=therapist_issue, back_populates="specialisations")

clients: so.Mapped[List["Client"]] = so.relationship(
secondary=client_issue, back_populates="issues"
)
therapists: so.Mapped[List["Therapist"]] = so.relationship(
secondary=therapist_issue, back_populates="specialisations"
)


class Intervention(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
name: so.Mapped[str] = so.mapped_column(sa.String(50), unique=True)

therapists: so.Mapped[List["Therapist"]] = so.relationship(secondary=therapist_intervention, back_populates="interventions")

therapists: so.Mapped[List["Therapist"]] = so.relationship(
secondary=therapist_intervention, back_populates="interventions"
)


class SessionType(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
therapist_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("therapist.id"), index=True)
name: so.Mapped[str] = so.mapped_column(sa.String(255)) # e.g. "Initial Consultation"
therapist_id: so.Mapped[int] = so.mapped_column(
sa.ForeignKey("therapist.id"), index=True
)
name: so.Mapped[str] = so.mapped_column(
sa.String(255)
) # e.g. "Initial Consultation"
session_duration: so.Mapped[int] = so.mapped_column(sa.Integer) # In minutes
fee_amount: so.Mapped[float] = so.mapped_column(sa.Float)
fee_currency: so.Mapped[str] = so.mapped_column(sa.String(3))
session_format: so.Mapped[Optional["SessionFormat"]] = so.mapped_column(sa.Enum(SessionFormat))
session_format: so.Mapped[Optional["SessionFormat"]] = so.mapped_column(
sa.Enum(SessionFormat)
)
notes: so.Mapped[Optional[str]] = so.mapped_column(sa.Text)

therapist: so.Mapped["Therapist"] = so.relationship(back_populates="session_types")


class Availability(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
therapist_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("therapist.id"), index=True)
day_of_week: so.Mapped[Optional[int]] = so.mapped_column(sa.Integer) # 0=Monday, 6=Sunday, None for specific dates
therapist_id: so.Mapped[int] = so.mapped_column(
sa.ForeignKey("therapist.id"), index=True
)
day_of_week: so.Mapped[Optional[int]] = so.mapped_column(
sa.Integer
) # 0=Monday, 6=Sunday, None for specific dates
start_time: so.Mapped[Optional[time]] = so.mapped_column(sa.Time)
end_time: so.Mapped[Optional[time]] = so.mapped_column(sa.Time)
specific_date: so.Mapped[Optional[date]] = so.mapped_column(sa.Date) # For non-recurring availability

specific_date: so.Mapped[Optional[date]] = so.mapped_column(
sa.Date
) # For non-recurring availability

therapist: so.Mapped["Therapist"] = so.relationship(back_populates="availabilities")


class Unavailability(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
therapist_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey("therapist.id"), index=True)
therapist_id: so.Mapped[int] = so.mapped_column(
sa.ForeignKey("therapist.id"), index=True
)
start_date: so.Mapped[date] = so.mapped_column(sa.Date)
end_date: so.Mapped[date] = so.mapped_column(sa.Date)
reason: so.Mapped[Optional[str]] = so.mapped_column(sa.Text)

therapist: so.Mapped["Therapist"] = so.relationship(back_populates="unavailabilities")

therapist: so.Mapped["Therapist"] = so.relationship(
back_populates="unavailabilities"
)


def insertDummyData() -> None:
users: List[User] = [
User(
email="[email protected]",
password_hash=generate_password_hash("password"),
first_name="John",
last_name="Smith",
date_joined=date.today(),
role=UserRole.CLIENT,
verified=True,
active=True,
gender=Gender.MALE,
),
User(
email="[email protected]",
password_hash=generate_password_hash("password"),
Expand Down
5 changes: 4 additions & 1 deletion app/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ input:-webkit-autofill:focus {
}

.btn-primary,
.btn-primary:disabled {
.btn-primary.active,
.btn-primary.show,
.btn-primary:disabled,
:not(.btn-check)+.btn:active {
color: white;
background: var(--colour-primary);
}
Expand Down
21 changes: 12 additions & 9 deletions app/static/js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ function showLoadingBtn(isLoading) {

// Function to display error messages
function displayFormErrors(errors) {

// Clear previous errors
$('.error-message').remove();
$('.input-error').removeClass('input-error');

// Display new errors below corresponding input fields
for (const key in errors) {
const inputField = $('#' + key);
const errorMessage = $(
Expand All @@ -33,10 +27,19 @@ function displayFormErrors(errors) {
}

function ajaxFormResponseHandler(response) {
if (response.errors) {
displayFormErrors(response.errors);

// Clear previous errors
$('.error-message').remove();
$('.input-error').removeClass('input-error');

if (response.success) {
if (response.url) {
window.location = response.url;
}
} else {
window.location = response.url;
if (response.errors) {
displayFormErrors(response.errors);
}
}
}

Expand Down
2 changes: 0 additions & 2 deletions app/utils/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ class EmailSubject(Enum):


class EmailMessage:

def __init__(
self,
mail: Mail,
subject: EmailSubject,
recipient: User,
serialiser: Optional[URLSafeTimedSerializer],
) -> None:

self.mail = mail
self.recipient = recipient
self.subject = subject.value
Expand Down
14 changes: 0 additions & 14 deletions app/utils/password.py

This file was deleted.

Loading

0 comments on commit 6323ce4

Please sign in to comment.