Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
62c7c37
Add module imports and implement scheduling strategies
renuah1142 Nov 25, 2025
7e476bc
Refactor Admin class to inherit from User and streamline initialization
renuah1142 Nov 25, 2025
9c59149
Implement scheduling methods in Admin class and enhance Staff class w…
renuah1142 Nov 26, 2025
ede1d43
Refactor scheduling logic and enhance Admin controller with auto-sche…
Nov 26, 2025
136d49c
Enforce admin permissions in scheduling and reporting functions; rais…
Nov 27, 2025
a177611
Remove unused test initialization file and update user creation test …
daniellamurray17-oss Nov 27, 2025
a744b6f
refactoring the url prefixes for existing routes v1
Andrews3002 Nov 27, 2025
c079ff3
refactoring the url prefixes for existing routes v2
Andrews3002 Nov 27, 2025
4851bf2
Refactor test suite: consolidate unit tests into test_app.py and remo…
renuah1142 Nov 27, 2025
c9d5d3a
Refactor test imports: remove unnecessary blank line and clean up tes…
renuah1142 Nov 27, 2025
3c6f35e
integrations test working (all) all test
renuah1142 Nov 27, 2025
a2856eb
autoscheduling fix v1
Andrews3002 Nov 29, 2025
70761ec
autoscheduling fix v2
Andrews3002 Nov 29, 2025
7ad13e9
autoscheduling fix v3
Andrews3002 Nov 29, 2025
48a99ba
autoscheduling fix v4
Andrews3002 Nov 29, 2025
34324ac
refactring existing url prefixes for existing routes v3
Andrews3002 Nov 30, 2025
e58aca6
refactring existing url prefixes for existing routes v4
Andrews3002 Nov 30, 2025
cfe37fb
refactring existing url prefixes for existing routes v5
Andrews3002 Nov 30, 2025
dc9aa95
Merge pull request #1 from SUICIDESQUAD4/refactor-models
Andrews3002 Dec 1, 2025
6263a42
adding integration tests to main
Andrews3002 Dec 1, 2025
50c1a45
merging the refactored_routes
Andrews3002 Dec 1, 2025
3b64fca
refactoring code for unit and integration tests to work as well as ha…
Andrews3002 Dec 1, 2025
4076418
refactoring code for unit and integration tests to work as well as ha…
Andrews3002 Dec 1, 2025
d90c1c9
fixing index / route
Andrews3002 Dec 2, 2025
12c2f4e
update deployment configuration and add runtime specification
renuah1142 Dec 3, 2025
b629b13
render.yaml and deployment env
renuah1142 Dec 3, 2025
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
1 change: 1 addition & 0 deletions .flaskenv
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#.flaskenv
FLASK_RUN_PORT=8080
FLASK_APP=wsgi.py
FLASK_DEBUG=True
1 change: 1 addition & 0 deletions App/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/__init__.py
from .models import *
from .views import *
from .controllers import *
Expand Down
1 change: 1 addition & 0 deletions App/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/config.py
import os

def load_config(app, overrides):
Expand Down
7 changes: 4 additions & 3 deletions App/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# App/controllers/_init_.py
from .user import *
from .auth import *
from .initialize import *
from .admin import *
from .staff import *
from .staff import *
from .auth import *
from .initialize import *
122 changes: 82 additions & 40 deletions App/controllers/admin.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,100 @@
from App.models import Shift
from App.models.strategy import ScheduleStrategyFactory
from App.models.auto_scheduler import AutoScheduler
from datetime import datetime, timedelta
import random
from App.models import Admin, Staff, Shift, Schedule
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

def create_schedule(admin_id, scheduleName): #Not sure why this was missing
admin = get_user(admin_id)
if not admin or admin.role != "admin":
raise PermissionError("Only admins can create schedules")
def random_shift_time(start_hour=6, end_hour=22, min_duration=4, max_duration=8):
day = datetime.now().date()

new_schedule = Schedule(
created_by=admin_id,
name=scheduleName,
created_at=datetime.utcnow()
)
start = datetime.combine(day, datetime.min.time()) + timedelta(hours=random.randint(start_hour, end_hour - min_duration))
duration = timedelta(hours=random.randint(min_duration, max_duration))
end = start + duration

db.session.add(new_schedule)
db.session.commit()
return start, end

return new_schedule
def generate_random_templates(schedule_id, num_templates):
shifts = []

def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time):
admin = get_user(admin_id)
staff = get_user(staff_id)
for _ in range(num_templates):
start, end = random_shift_time()
shift = Shift(
staff_id=None,
schedule_id=schedule_id,
start_time=start,
end_time=end
)
shifts.append(shift)

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

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")
def auto_schedule(schedule_id: int, method_type: str):
schedule = Schedule.query.filter_by(id=schedule_id).first()
if not schedule:
raise ValueError("Invalid schedule ID")
return {"status": "error", "message": "Schedule not found"}

staff_list = Staff.query.all()

num = staff_list.__len__()
if num == 0:
return {"status": "error", "message": "No staff available for scheduling"}

shift_templates = generate_random_templates(schedule_id, num_templates=num)

print(shift_templates)

new_shift = Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=start_time,
end_time=end_time
)
try:
strategy = ScheduleStrategyFactory.create_strategy(method_type)
except ValueError as e:
return {"status": "error", "message": str(e)}

scheduler = AutoScheduler(strategy, staff_list, shift_templates, schedule_id)
result = scheduler.generate_schedule()

try:
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "message": f"Auto-scheduling failed: {e}"}

db.session.add(new_shift)
db.session.commit()

return new_shift
def schedule_shift(admin_id: int, staff_id: int, schedule_id: int, start_time, end_time):
actor = get_user(admin_id)
if not actor or actor.role != "admin":
raise PermissionError("Only admins can schedule shifts")
if not staff_id or not schedule_id:
raise ValueError("staff_id and schedule_id are required")

# parse datetimes if strings
if isinstance(start_time, str):
start_time = datetime.fromisoformat(start_time)
if isinstance(end_time, str):
end_time = datetime.fromisoformat(end_time)

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

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

schedule = Schedule(created_by=admin_id, admin_id=admin_id, staff_id=None, name=f"Schedule_{schedule_id}_{datetime.utcnow().isoformat()}")

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

def get_shift_report(admin_id):
admin = get_user(admin_id)
if not admin or admin.role != "admin":

def get_shift_report(admin_id: int):
actor = get_user(admin_id)
if not actor or actor.role != "admin":
raise PermissionError("Only admins can view shift reports")

return [shift.get_json() for shift in Shift.query.order_by(Shift.start_time).all()]
shifts = Shift.query.order_by(Shift.start_time).all()
return [s.get_json() for s in shifts]

def viewShift(shift_id: int):
shift = Shift.query.get(shift_id)
if not shift:
raise ValueError("Shift not found")
return shift.get_json()
6 changes: 2 additions & 4 deletions App/controllers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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

Expand All @@ -27,7 +27,7 @@ def loginCLI(username, password):
db.session.commit()
return {"message": "Login successful", "token": token}

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

def logout(username):
result = db.session.execute(db.select(User).filter_by(username=username))
Expand All @@ -46,7 +46,6 @@ def logout(username):
def setup_jwt(app):
jwt = JWTManager(app)

# Always store a string user id in the JWT identity (sub)
@jwt.user_identity_loader
def user_identity_lookup(identity):
user_id = getattr(identity, "id", identity)
Expand All @@ -63,7 +62,6 @@ def user_lookup_callback(_jwt_header, jwt_data):

return jwt

# Context processor to make 'is_authenticated' available to all templates
def add_auth_context(app):
@app.context_processor
def inject_user():
Expand Down
22 changes: 2 additions & 20 deletions App/controllers/initialize.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#app/controllers/initialize.py
from .user import create_user
from App.database import db

Expand All @@ -8,23 +9,4 @@ def initialize():
create_user('bob', 'bobpass', 'admin')
create_user('jane', 'janepass', 'staff')
create_user('alice', 'alicepass', 'staff')
create_user('tim', 'timpass', 'user')

# 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)
create_user('tim', 'timpass', 'user')
2 changes: 1 addition & 1 deletion App/controllers/staff.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def get_combined_roster(staff_id):
staff = get_user(staff_id)
if not staff or staff.role != "staff":
raise PermissionError("Only staff can view roster")
return [shift.get_json() for shift in Shift.query.order_by(Shift.start_time).all()]
return [shift.get_json() for shift in Shift.query.all()]


def clock_in(staff_id, shift_id):
Expand Down
5 changes: 2 additions & 3 deletions App/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
def create_user(username, password, role):
role = role.lower().strip()
if role not in VALID_ROLES:
print(f"⚠️ Invalid role '{role}'. Must be one of {VALID_ROLES}")
return None
raise ValueError(f"Invalid role '{role}'. Must be one of {VALID_ROLES}")
if role == "admin":
newuser = Admin(username=username, password=password)
elif role == "staff":
Expand Down Expand Up @@ -41,4 +40,4 @@ def update_user(id, username):
user.username = username
db.session.commit()
return user
return None
return None
1 change: 1 addition & 0 deletions App/database.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# database.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

Expand Down
1 change: 1 addition & 0 deletions App/default_config.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# App/default_config.py
SQLALCHEMY_DATABASE_URI="sqlite:///temp-database.db"
SECRET_KEY="secret key"
1 change: 1 addition & 0 deletions App/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/main.py
import os
from flask import Flask, render_template
from flask_uploads import DOCUMENTS, IMAGES, TEXT, UploadSet, configure_uploads
Expand Down
10 changes: 8 additions & 2 deletions App/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
from App.models.admin import Admin
from App.models.staff import Staff
from App.models.schedule import Schedule
from App.models.shift import Shift

from App.models.shift import Shift
from App.models.auto_scheduler import AutoScheduler
from App.models.strategy import (
ScheduleStrategy,
EvenDistribution,
MinimalDays,
BalancedShift
)
66 changes: 66 additions & 0 deletions App/models/admin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
from datetime import datetime
from App.database import db
from .user import User


class Admin(User):
id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)

__mapper_args__ = {
"polymorphic_identity": "admin",
}

def __init__(self, username, password):
super().__init__(username, password, "admin")

def schedule_shift(self, staff_id: int, schedule_id: int, start, end):
from App.models.shift import Shift

if not isinstance(start, datetime):
start_dt = datetime.fromisoformat(start)
else:
start_dt = start

if not isinstance(end, datetime):
end_dt = datetime.fromisoformat(end)
else:
end_dt = end

shift = Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=start_dt,
end_time=end_dt,
)
db.session.add(shift)
db.session.commit()
return shift

def auto_schedule(self, schedule_id: int, method_type: str):
from App.models.auto_scheduler import AutoScheduler
from App.models.strategy import (
EvenDistributionStrategy,
MinimalDaysStrategy,
BalancedShiftStrategy,
)
from App.models.shift import Shift
from App.models.staff import Staff

staff_list = Staff.query.all()

shift_templates = Shift.query.filter_by(schedule_id=schedule_id, staff_id=None).all()

scheduler = AutoScheduler(staff_list, shift_templates)

method = method_type.lower() if isinstance(method_type, str) else str(method_type)
if method in ("even", "even_distribution", "even distribution"):
strategy = EvenDistributionStrategy()
elif method in ("minimal", "minimal_days", "minimal days"):
strategy = MinimalDaysStrategy()
else:
strategy = BalancedShiftStrategy()

scheduler.set_strategy(strategy)
assigned = scheduler.generate_schedule(schedule_id)

for s in assigned:
db.session.add(s)
db.session.commit()

return [s.get_json() for s in assigned]

def view_shift(self, shift_id: int):
from App.models.shift import Shift

shift = db.session.get(Shift, shift_id)
return shift.get_json() if shift else None

Loading