Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
496 changes: 496 additions & 0 deletions API_REFERENCE.md

Large diffs are not rendered by default.

40 changes: 35 additions & 5 deletions App/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
from .user import *
from .auth import *
from .initialize import *
from .admin import *
from .staff import *
# User management
from .user import (
create_user,
get_user,
get_user_by_username,
get_all_users,
get_all_users_json,
update_user
)

# Authentication
from .auth import login, loginCLI, logout

# Initialize database
from .initialize import initialize

# Staff actions
from .staff import (
get_combined_roster,
clock_in,
clock_out,
get_shift
)

# Admin schedule functions
from .admin import (
create_schedule,
add_shift,
auto_populate_schedule,
get_schedule_report
)

# Schedule controller (class)
from .schedule_controller import ScheduleController

57 changes: 18 additions & 39 deletions App/controllers/admin.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,37 @@
from App.models import Shift
from App.database import db
from datetime import datetime
from App.controllers.user import get_user

from App.models import Shift, Schedule
from App.database import db
from datetime import datetime
from App.controllers.user import get_user
from App.models.admin import Admin
from App.controllers.schedule_controller import ScheduleController

def create_schedule(admin_id, scheduleName): #Not sure why this was missing
def create_schedule(admin_id, schedule_name, user_id=None):
"""Allow an admin to create a new schedule."""
admin = get_user(admin_id)
if not admin or admin.role != "admin":
raise PermissionError("Only admins can create schedules")

new_schedule = Schedule(
created_by=admin_id,
name=scheduleName,
created_at=datetime.utcnow()
)

db.session.add(new_schedule)
db.session.commit()

return new_schedule
return ScheduleController.create_schedule(admin_id, schedule_name, user_id)

def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time):
def add_shift(admin_id, staff_id, schedule_id, start_time, end_time, shift_type="day"):
"""Allow an admin to manually add a shift."""
admin = get_user(admin_id)
staff = get_user(staff_id)

schedule = db.session.get(Schedule, schedule_id)

if not admin or admin.role != "admin":
raise PermissionError("Only admins can schedule shifts")
if not staff or staff.role != "staff":
raise ValueError("Invalid staff member")
if not schedule:
raise ValueError("Invalid schedule ID")

new_shift = Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=start_time,
end_time=end_time
)

db.session.add(new_shift)
db.session.commit()
return ScheduleController.add_shift(schedule_id, staff_id, start_time, end_time, shift_type)

return new_shift
def auto_populate_schedule(admin_id, schedule_id, strategy_name):
"""Allow an admin to auto-populate shifts using a strategy."""
admin = get_user(admin_id)
if not admin or admin.role != "admin":
raise PermissionError("Only admins can populate schedules")

return ScheduleController.auto_populate(schedule_id, strategy_name)

def get_shift_report(admin_id):
def get_schedule_report(admin_id, schedule_id):
"""Allow an admin to view the schedule report."""
admin = get_user(admin_id)
if not admin or admin.role != "admin":
raise PermissionError("Only admins can view shift reports")
raise PermissionError("Only admins can view schedule reports")

return [shift.get_json() for shift in Shift.query.order_by(Shift.start_time).all()]
return ScheduleController.get_Schedule_report(schedule_id)
67 changes: 43 additions & 24 deletions App/controllers/auth.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,99 @@
from flask import jsonify
from flask_jwt_extended import (
create_access_token, jwt_required, JWTManager,
get_jwt_identity, verify_jwt_in_request
get_jwt_identity, set_access_cookies, verify_jwt_in_request
)
from App.models import User
from App.models import User, user
from App.database import db

def _get_user_by_username(username):
"""Fetch a user object by username."""
result = db.session.execute(db.select(User).filter_by(username=username))
return result.scalar_one_or_none()

def login(username, password):
result = db.session.execute(db.select(User).filter_by(username=username))
user = result.scalar_one_or_none()
if user and user.check_password(password):
# Store ONLY the user id as a string in JWT 'sub'
return create_access_token(identity=str(user.id))
return None
user = _get_user_by_username(username)
if user and user.check_password(password):
token = create_access_token(identity=user)
return token
return None


def loginCLI(username, password):
result = db.session.execute(db.select(User).filter_by(username=username))
user = result.scalar_one_or_none()
user = _get_user_by_username(username)

if user and user.check_password(password):


# Return existing token if already logged in
if user.active_token:
return {"message": "User already logged in", "token": user.active_token}

# Generate new token
token = create_access_token(identity=str(user.id))
user.active_token = token
db.session.commit()

return {"message": "Login successful", "token": token}

return {"message": "Invalid username or password"}


def logout(username):
result = db.session.execute(db.select(User).filter_by(username=username))
user = result.scalar_one_or_none()
user = _get_user_by_username(username)

if not user:
return {"message": "User not found"}

if not user.active_token:
return {"message": f"User {username} is not logged in"}
return {"message": f"User '{username}' is not logged in"}

user.active_token = None
db.session.commit()
return {"message": f"User {username} logged out successfully"}

return {"message": f"User '{username}' logged out successfully"}


def setup_jwt(app):
jwt = JWTManager(app)

# Always store a string user id in the JWT identity (sub)
# Always store user.id (as string) in JWT
@jwt.user_identity_loader
def user_identity_lookup(identity):
user_id = getattr(identity, "id", identity)
return str(user_id) if user_id is not None else None

# Automatically load user from JWT on request
@jwt.user_lookup_loader
def user_lookup_callback(_jwt_header, jwt_data):
identity = jwt_data["sub"]
identity = jwt_data.get("sub")
try:
user_id = int(identity)
return db.session.get(User, int(identity))
except (TypeError, ValueError):
return None
return db.session.get(User, user_id)

return jwt

# Context processor to make 'is_authenticated' available to all templates
def add_auth_context(app):
@app.context_processor
def inject_user():
try:
verify_jwt_in_request()
identity = get_jwt_identity()
user_id = int(identity) if identity is not None else None
current_user = db.session.get(User, user_id) if user_id is not None else None

current_user = (
db.session.get(User, user_id)
if user_id is not None else None
)

is_authenticated = current_user is not None
except Exception as e:
print(e)

except Exception:
# Invalid or missing JWT
is_authenticated = False
current_user = None
return dict(is_authenticated=is_authenticated, current_user=current_user)

return dict(
is_authenticated=is_authenticated,
current_user=current_user
)
34 changes: 19 additions & 15 deletions App/controllers/initialize.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .user import create_user
from App.models.schedule import Schedule
from App.models.shift import Shift
from App.database import db


Expand All @@ -10,21 +12,23 @@ def initialize():
create_user('alice', 'alicepass', 'staff')
create_user('tim', 'timpass', 'user')

# db.session.commit()
db.session.commit()

# # adding dummy schedule data for testing Jane
# schedule = Schedule (
# name = "Morning Shift",
# created_by = 1
# )
# db.session.add(schedule)
# db.session.commit()
#adding dummy schedule data for testing Jane
schedule = Schedule (
name = "Morning Shift",
created_by = 1
)
db.session.add(schedule)
db.session.commit()

# # adding dummy shifts for Jane
# shift1 = Shift (
# schedule_id = schedule.id,
# staff_id = 2,
# start_time = "2024-10-01 08:00:00",
# end_time = "2024-10-01 12:00:00"
# )
# db.session.add(shift1)
shift1 = Shift (
schedule_id = schedule.id,
staff_id = 2,
start_time = "2024-10-01 08:00:00",
end_time = "2024-10-01 12:00:00"
)
db.session.add(shift1)

#shift2 = Shift(staff_id=2, schedule_id=schedule.id, start_time="2024-10-01 12:00:00", end_time="2024-10-01 16:00:00")
82 changes: 82 additions & 0 deletions App/controllers/schedule_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from App.database import db
from App.models.schedule import Schedule
from App.models.shift import Shift
from App.models import Staff, Admin
from datetime import datetime

# Import strategies
from App.models.strategies.even_distribution import EvenDistributionStrategy
from App.models.strategies.minimize_days import MinimizeDaysStrategy
from App.models.strategies.balance_day_night import BalanceDayNightStrategy

class ScheduleController:
"""Controller to manage schedules and auto-assign shifts using strategies."""

@staticmethod
def create_schedule(admin_id, name, user_id=None):
"""Create a new schedule, optionally for a specific user.
Note: Permission checking is done in admin controller."""
new_schedule = Schedule(
name=name,
created_by=admin_id,
user_id=user_id
)
db.session.add(new_schedule)
db.session.commit()
return new_schedule

@staticmethod
def add_shift(schedule_id, staff_id, start_time, end_time, shift_type="day"):
"""Add a shift for a specific staff to a schedule."""
schedule = db.session.get(Schedule, schedule_id)
staff = db.session.get(Staff, staff_id)
if not schedule or not staff:
raise ValueError("Invalid schedule or staff")

shift = Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=start_time,
end_time=end_time,
)
# Optional type attribute for day/night shifts
shift.type = shift_type

db.session.add(shift)
db.session.commit()
return shift

@staticmethod
def auto_populate(schedule_id, strategy_name):
"""Auto-populate the shifts of a schedule using a strategy."""
schedule = db.session.get(Schedule, schedule_id)
if not schedule:
raise ValueError("Schedule not found")

staff_list = Staff.query.all()
shift_list = schedule.shifts # Existing shifts in the schedule

# Assign strategy
if strategy_name == "even_distribution":
strategy = EvenDistributionStrategy()
elif strategy_name == "minimize_days":
strategy = MinimizeDaysStrategy()
elif strategy_name == "balance_day_night":
strategy = BalanceDayNightStrategy()
else:
raise ValueError("Invalid strategy name")

# Generate schedule using the strategy
updated_shifts = strategy.generate(staff_list, shift_list)

# Commit updated staff assignments
db.session.commit()
return updated_shifts

@staticmethod
def get_Schedule_report(schedule_id):
"""Return JSON data for a schedule and its shifts."""
schedule = db.session.get(Schedule, schedule_id)
if not schedule:
raise ValueError("Schedule not found")
return schedule.get_json()
Loading