From 735a42fc9bac8eb197d9c5f516138060f078413d Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:30:54 -0400 Subject: [PATCH 01/18] Refactor admin controller Removed duplicates & improve structure --- App/controllers/admin.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/App/controllers/admin.py b/App/controllers/admin.py index aa2d9ca..f018134 100644 --- a/App/controllers/admin.py +++ b/App/controllers/admin.py @@ -1,39 +1,39 @@ -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.models import Shift, Schedule from App.controllers.user import get_user -def create_schedule(admin_id, scheduleName): #Not sure why this was missing + +def _assert_admin(admin_id): admin = get_user(admin_id) if not admin or admin.role != "admin": - raise PermissionError("Only admins can create schedules") + raise PermissionError("Only admins can perform this action") + return admin + + +def create_schedule(admin_id, schedule_name): + _assert_admin(admin_id) new_schedule = Schedule( created_by=admin_id, - name=scheduleName, + name=schedule_name, created_at=datetime.utcnow() ) db.session.add(new_schedule) db.session.commit() - return new_schedule -def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time): - admin = get_user(admin_id) - staff = get_user(staff_id) - schedule = db.session.get(Schedule, schedule_id) +def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time): + _assert_admin(admin_id) - if not admin or admin.role != "admin": - raise PermissionError("Only admins can schedule shifts") + staff = get_user(staff_id) if not staff or staff.role != "staff": raise ValueError("Invalid staff member") + + schedule = db.session.get(Schedule, schedule_id) if not schedule: raise ValueError("Invalid schedule ID") @@ -46,13 +46,11 @@ def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time): db.session.add(new_shift) db.session.commit() - return new_shift def get_shift_report(admin_id): - admin = get_user(admin_id) - if not admin or admin.role != "admin": - raise PermissionError("Only admins can view shift reports") + _assert_admin(admin_id) - return [shift.get_json() for shift in Shift.query.order_by(Shift.start_time).all()] \ No newline at end of file + shifts = Shift.query.order_by(Shift.start_time).all() + return [shift.get_json() for shift in shifts] From 870cbfd307076e78fef250dcc7086f952c956d09 Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:37:59 +0000 Subject: [PATCH 02/18] Refactor auth controller --- App/controllers/auth.py | 68 ++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/App/controllers/auth.py b/App/controllers/auth.py index e46a40f..8bdc54a 100644 --- a/App/controllers/auth.py +++ b/App/controllers/auth.py @@ -5,65 +5,79 @@ from App.models import 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): + return create_access_token(identity=str(user.id)) + + 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 + +# ---------------------------- +# Template Context: Authentication State +# ---------------------------- + def add_auth_context(app): @app.context_processor def inject_user(): @@ -71,10 +85,20 @@ def inject_user(): 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 + ) From 445c3c91b1ac531b19a85a45c8cc600b341ce530 Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:03:49 +0000 Subject: [PATCH 03/18] Refactored auth and admin controllers --- App/controllers/auth.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/App/controllers/auth.py b/App/controllers/auth.py index 8bdc54a..cdce182 100644 --- a/App/controllers/auth.py +++ b/App/controllers/auth.py @@ -73,11 +73,6 @@ def user_lookup_callback(_jwt_header, jwt_data): return jwt - -# ---------------------------- -# Template Context: Authentication State -# ---------------------------- - def add_auth_context(app): @app.context_processor def inject_user(): From 01cc6ec5f4794d9a0703af595557d584f64940fb Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:16:54 +0000 Subject: [PATCH 04/18] Refactor staff controller with helpers --- App/controllers/staff.py | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/App/controllers/staff.py b/App/controllers/staff.py index 6c21d3a..f6002cc 100644 --- a/App/controllers/staff.py +++ b/App/controllers/staff.py @@ -1,24 +1,33 @@ -from App.models import Shift -from App.database import db from datetime import datetime + +from App.database import db +from App.models import Shift from App.controllers.user import get_user -def get_combined_roster(staff_id): +def _assert_staff(staff_id): + """Ensure the user exists and has the 'staff' role.""" 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()] + raise PermissionError("Only staff members can perform this action") + return staff -def clock_in(staff_id, shift_id): - staff = get_user(staff_id) - if not staff or staff.role != "staff": - raise PermissionError("Only staff can clock in") - +def _get_shift_for_staff(staff_id, shift_id): + """Fetch a shift and verify it belongs to the given staff member.""" shift = db.session.get(Shift, shift_id) - if not shift or shift.staff_id != staff_id: raise ValueError("Invalid shift for staff") + return shift + +def get_combined_roster(staff_id): + _assert_staff(staff_id) + shifts = Shift.query.order_by(Shift.start_time).all() + return [shift.get_json() for shift in shifts] + + +def clock_in(staff_id, shift_id): + _assert_staff(staff_id) + shift = _get_shift_for_staff(staff_id, shift_id) shift.clock_in = datetime.now() db.session.commit() @@ -26,18 +35,13 @@ def clock_in(staff_id, shift_id): def clock_out(staff_id, shift_id): - staff = get_user(staff_id) - if not staff or staff.role != "staff": - raise PermissionError("Only staff can clock out") - - shift = db.session.get(Shift, shift_id) - if not shift or shift.staff_id != staff_id: - raise ValueError("Invalid shift for staff") + _assert_staff(staff_id) + shift = _get_shift_for_staff(staff_id, shift_id) shift.clock_out = datetime.now() db.session.commit() return shift + def get_shift(shift_id): - shift = db.session.get(Shift, shift_id) - return shift \ No newline at end of file + return db.session.get(Shift, shift_id) From c19973d33f0112333119ff70cb48b6470705c8c5 Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Fri, 21 Nov 2025 03:24:38 +0000 Subject: [PATCH 05/18] Refactored user controllers --- App/controllers/user.py | 45 +++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/App/controllers/user.py b/App/controllers/user.py index 7570136..49e5b9f 100644 --- a/App/controllers/user.py +++ b/App/controllers/user.py @@ -1,42 +1,57 @@ -from App.models import User, Admin, Staff, Shift -from App.database import db from datetime import datetime +from App.database import db +from App.models import User, Admin, Staff + VALID_ROLES = {"user", "staff", "admin"} +def _normalize_role(role): + """Normalize role to lowercase and strip spaces.""" + return role.lower().strip() + + def create_user(username, password, role): - role = role.lower().strip() + role = _normalize_role(role) if role not in VALID_ROLES: print(f"⚠️ Invalid role '{role}'. Must be one of {VALID_ROLES}") return None + if role == "admin": - newuser = Admin(username=username, password=password) + new_user = Admin(username=username, password=password) elif role == "staff": - newuser = Staff(username=username, password=password) + new_user = Staff(username=username, password=password) else: - newuser = User(username=username, password=password, role="user") + new_user = User(username=username, password=password, role="user") - db.session.add(newuser) + db.session.add(new_user) db.session.commit() - return newuser + return new_user + + +def get_user(user_id): + """Fetch a user by ID.""" + return db.session.get(User, user_id) + def get_user_by_username(username): + """Fetch a user by username.""" return User.query.filter_by(username=username).first() -def get_user(id): - return db.session.get(User, id) def get_all_users(): + """Return all user objects.""" return User.query.all() + def get_all_users_json(): + """Return all users as JSON objects.""" users = get_all_users() - if not users: - return [] - return [user.get_json() for user in users] + return [user.get_json() for user in users] if users else [] + -def update_user(id, username): - user = get_user(id) +def update_user(user_id, username): + """Update a user's username.""" + user = get_user(user_id) if user: user.username = username db.session.commit() From cf2ef5520b9aab3a101380ad13b8dbea5059ec2d Mon Sep 17 00:00:00 2001 From: andreaselenaa <159645343+andreaselenaa@users.noreply.github.com> Date: Sat, 22 Nov 2025 02:49:06 +0000 Subject: [PATCH 06/18] Refactor admin controller & schedule with strategy pattern --- App/controllers/admin.py | 67 ++++++--------- App/controllers/schedule_controller.py | 86 +++++++++++++++++++ App/models/admin.py | 18 +++- App/models/schedule.py | 17 +++- App/models/shift.py | 42 ++++++++- App/models/staff.py | 42 +++++++++ App/models/strategies.py/__init__.py | 12 +++ App/models/strategies.py/balance_day_night.py | 18 ++++ App/models/strategies.py/even_distribution.py | 13 +++ App/models/strategies.py/minimize_days.py | 16 ++++ App/models/strategies.py/schedule_strategy.py | 8 ++ App/models/user.py | 19 +--- 12 files changed, 294 insertions(+), 64 deletions(-) create mode 100644 App/controllers/schedule_controller.py create mode 100644 App/models/strategies.py/__init__.py create mode 100644 App/models/strategies.py/balance_day_night.py create mode 100644 App/models/strategies.py/even_distribution.py create mode 100644 App/models/strategies.py/minimize_days.py create mode 100644 App/models/strategies.py/schedule_strategy.py diff --git a/App/controllers/admin.py b/App/controllers/admin.py index f018134..f1a7db8 100644 --- a/App/controllers/admin.py +++ b/App/controllers/admin.py @@ -1,56 +1,37 @@ from datetime import datetime - from App.database import db -from App.models import Shift, Schedule from App.controllers.user import get_user +from App.models.admin import Admin +from App.controllers.schedule_controller import ScheduleController - -def _assert_admin(admin_id): +def create_schedule(admin_id, schedule_name): + """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 perform this action") - return admin - - -def create_schedule(admin_id, schedule_name): - _assert_admin(admin_id) - - new_schedule = Schedule( - created_by=admin_id, - name=schedule_name, - created_at=datetime.utcnow() - ) - - db.session.add(new_schedule) - db.session.commit() - return new_schedule + raise PermissionError("Only admins can create schedules") + return ScheduleController.create_schedule(admin_id, schedule_name) -def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time): - _assert_admin(admin_id) - - staff = get_user(staff_id) - if not staff or staff.role != "staff": - raise ValueError("Invalid staff member") - - schedule = db.session.get(Schedule, schedule_id) - if not schedule: - raise ValueError("Invalid schedule ID") +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) + if not admin or admin.role != "admin": + raise PermissionError("Only admins can schedule shifts") - new_shift = Shift( - staff_id=staff_id, - schedule_id=schedule_id, - start_time=start_time, - end_time=end_time - ) + return ScheduleController.add_shift(schedule_id, staff_id, start_time, end_time, shift_type) - db.session.add(new_shift) - db.session.commit() - 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): - _assert_admin(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 schedule reports") - shifts = Shift.query.order_by(Shift.start_time).all() - return [shift.get_json() for shift in shifts] + return ScheduleController.get_schedule_report(schedule_id) diff --git a/App/controllers/schedule_controller.py b/App/controllers/schedule_controller.py new file mode 100644 index 0000000..a729595 --- /dev/null +++ b/App/controllers/schedule_controller.py @@ -0,0 +1,86 @@ +from App.database import db +from App.models.schedule import Schedule +from App.models.shift import Shift +from App.models.user import Staff, Admin +from App.models.admin import 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): + """Create a new schedule for the admin.""" + admin = db.session.get(Admin, admin_id) + if not admin: + raise PermissionError("Only admins can create schedules") + + new_schedule = Schedule( + name=name, + created_by=admin_id, + created_at=datetime.utcnow() + ) + 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() diff --git a/App/models/admin.py b/App/models/admin.py index 479832a..471b537 100644 --- a/App/models/admin.py +++ b/App/models/admin.py @@ -1,5 +1,11 @@ + from App.database import db -from .user import User +from App.models.user import User + +from App.models.strategies.schedule_strategy import ScheduleStrategy +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 Admin(User): id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) @@ -9,3 +15,13 @@ class Admin(User): def __init__(self, username, password): super().__init__(username, password, "admin") + self.schedule_strategy = None + + # Strategy pattern methods + def set_schedule_strategy(self, strategy: ScheduleStrategy): + self.schedule_strategy = strategy + + def generate_schedule(self, staff_list, shift_list): + if not self.schedule_strategy: + raise ValueError("No strategy assigned") + return self.schedule_strategy.generate(staff_list, shift_list) diff --git a/App/models/schedule.py b/App/models/schedule.py index 64c0e24..f0c1239 100644 --- a/App/models/schedule.py +++ b/App/models/schedule.py @@ -2,15 +2,28 @@ from App.database import db class Schedule(db.Model): + """ + full schedule containing multiple shifts. + """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) created_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) - shifts = db.relationship("Shift", backref="schedule", lazy=True) + + # One-to-many relationship with Shift + shifts = db.relationship("Shift", backref="schedule", lazy=True, cascade="all, delete-orphan") + + strategy_used = db.Column(db.String(50), nullable=True) def shift_count(self): return len(self.shifts) + def set_strategy_used(self, strategy): + + self.strategy_used = strategy.__class__.__name__ + def get_json(self): return { "id": self.id, @@ -18,7 +31,9 @@ def get_json(self): "created_at": self.created_at.isoformat(), "created_by": self.created_by, "shift_count": self.shift_count(), + "strategy_used": self.strategy_used, "shifts": [shift.get_json() for shift in self.shifts] } + diff --git a/App/models/shift.py b/App/models/shift.py index 0467dee..dcf1649 100644 --- a/App/models/shift.py +++ b/App/models/shift.py @@ -2,24 +2,60 @@ from App.database import db class Shift(db.Model): + id = db.Column(db.Integer, primary_key=True) + + # Who the shift belongs to staff_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + + # Which schedule this shift is part of schedule_id = db.Column(db.Integer, db.ForeignKey("schedule.id"), nullable=True) + + # Time range for the shift start_time = db.Column(db.DateTime, nullable=False) end_time = db.Column(db.DateTime, nullable=False) + + # Times of clock in or out clock_in = db.Column(db.DateTime, nullable=True) clock_out = db.Column(db.DateTime, nullable=True) - staff = db.relationship("Staff", backref="shifts", foreign_keys=[staff_id]) + staff = db.relationship( + "Staff", + backref="shifts", + foreign_keys=[staff_id], + lazy=True + ) + + @property + def is_completed(self): + return self.clock_in is not None and self.clock_out is not None + + @property + def is_active_shift(self): + """True if now is between start_time and end_time.""" + now = datetime.now() + return self.start_time <= now <= self.end_time + + @property + def is_late(self): + """True if the staff clocked in after shift start.""" + return self.clock_in and self.clock_in > self.start_time def get_json(self): return { "id": self.id, "staff_id": self.staff_id, "staff_name": self.staff.username if self.staff else None, - "start_time": self.start_time.isoformat(), "schedule_id": self.schedule_id, + + "start_time": self.start_time.isoformat(), "end_time": self.end_time.isoformat(), + "clock_in": self.clock_in.isoformat() if self.clock_in else None, - "clock_out": self.clock_out.isoformat() if self.clock_out else None + "clock_out": self.clock_out.isoformat() if self.clock_out else None, + + "is_completed": self.is_completed, + "is_active_shift": self.is_active_shift, + "is_late": self.is_late, } + diff --git a/App/models/staff.py b/App/models/staff.py index bc2592a..0a80656 100644 --- a/App/models/staff.py +++ b/App/models/staff.py @@ -1,11 +1,53 @@ from App.database import db from .user import User +from datetime import datetime, timedelta class Staff(User): + id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + __mapper_args__ = { "polymorphic_identity": "staff", } + # ---------- Constructor ---------- def __init__(self, username, password): super().__init__(username, password, "staff") + + @property + def upcoming_shifts(self): + """Return shifts starting after now.""" + now = datetime.now() + return sorted([s for s in self.shifts if s.start_time > now], key=lambda s: s.start_time) + + @property + def current_shift(self): + """Return the shift currently in progress.""" + now = datetime.now() + for shift in self.shifts: + if shift.start_time <= now <= shift.end_time: + return shift + return None + + @property + def total_hours_scheduled(self): + """Total hours scheduled across all shifts.""" + total = timedelta() + for shift in self.shifts: + total += (shift.end_time - shift.start_time) + return total.total_seconds() / 3600 # convert to hours + + @property + def completed_shifts(self): + return [s for s in self.shifts if s.is_completed] + + @property + def get_json(self): + """Return Staff-specific JSON for frontend components.""" + return { + "id": self.id, + "username": self.username, + "role": "staff", + "total_hours_scheduled": self.total_hours_scheduled, + "upcoming_shift_count": len(self.upcoming_shifts), + } diff --git a/App/models/strategies.py/__init__.py b/App/models/strategies.py/__init__.py new file mode 100644 index 0000000..e7a267d --- /dev/null +++ b/App/models/strategies.py/__init__.py @@ -0,0 +1,12 @@ + +from .schedule_strategy import ScheduleStrategy +from .even_distribution import EvenDistributionStrategy +from .minimize_days import MinimizeDaysStrategy +from .balance_day_night import BalanceDayNightStrategy + +__all__ = [ + "ScheduleStrategy", + "EvenDistributionStrategy", + "MinimizeDaysStrategy", + "BalanceDayNightStrategy" +] diff --git a/App/models/strategies.py/balance_day_night.py b/App/models/strategies.py/balance_day_night.py new file mode 100644 index 0000000..0192da4 --- /dev/null +++ b/App/models/strategies.py/balance_day_night.py @@ -0,0 +1,18 @@ +from .schedule_strategy import ScheduleStrategy + +class BalanceDayNightStrategy(ScheduleStrategy): + """Distribute day/night shifts to prevent imbalance.""" + + def generate(self, staff_list, shift_list): + result = [] + night_count = {s.id: 0 for s in staff_list} + + for shift in shift_list: + if getattr(shift, "type", "day") == "night": + staff_id = min(night_count, key=night_count.get) + night_count[staff_id] += 1 + else: + staff_id = staff_list[0].id + shift.staff_id = staff_id + result.append(shift) + return result diff --git a/App/models/strategies.py/even_distribution.py b/App/models/strategies.py/even_distribution.py new file mode 100644 index 0000000..a7efba9 --- /dev/null +++ b/App/models/strategies.py/even_distribution.py @@ -0,0 +1,13 @@ +from .schedule_strategy import ScheduleStrategy + +class EvenDistributionStrategy(ScheduleStrategy): + """Assign shifts evenly across staff.""" + + def generate(self, staff_list, shift_list): + result = [] + n = len(staff_list) + for i, shift in enumerate(shift_list): + staff = staff_list[i % n] + shift.staff_id = staff.id + result.append(shift) + return result diff --git a/App/models/strategies.py/minimize_days.py b/App/models/strategies.py/minimize_days.py new file mode 100644 index 0000000..63aa1df --- /dev/null +++ b/App/models/strategies.py/minimize_days.py @@ -0,0 +1,16 @@ +from .schedule_strategy import ScheduleStrategy + +class MinimizeDaysStrategy(ScheduleStrategy): + """Distribute shifts to minimize number of workdays per staff.""" + + def generate(self, staff_list, shift_list): + result = [] + work_count = {s.id: 0 for s in staff_list} + + for shift in shift_list: + staff_id = min(work_count, key=work_count.get) + shift.staff_id = staff_id + work_count[staff_id] += 1 + result.append(shift) + return result + diff --git a/App/models/strategies.py/schedule_strategy.py b/App/models/strategies.py/schedule_strategy.py new file mode 100644 index 0000000..9966d10 --- /dev/null +++ b/App/models/strategies.py/schedule_strategy.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + +class ScheduleStrategy(ABC): + """Base class for schedule generation strategies.""" + + @abstractmethod + def generate(self, staff_list, shift_list): + pass diff --git a/App/models/user.py b/App/models/user.py index 41f2e6d..c576f56 100644 --- a/App/models/user.py +++ b/App/models/user.py @@ -1,6 +1,5 @@ from werkzeug.security import check_password_hash, generate_password_hash from App.database import db -from datetime import datetime class User(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -12,24 +11,12 @@ class User(db.Model): __mapper_args__ = { "polymorphic_identity": "user", "polymorphic_on": "role" - } - + } + def __init__(self, username, password, role="user"): self.username = username self.role = role - self.set_password(password) - - def get_json(self): - return { - 'id': self.id, - 'username': self.username, - 'role': self.role - } - - def set_password(self, password): self.password = generate_password_hash(password) - + def check_password(self, password): return check_password_hash(self.password, password) - - From 1a6f8a6d0f50457026dfeacbb29d40eac7aad329 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:15:18 -0400 Subject: [PATCH 07/18] Update user.py Reviewed and edited refactored code From 8c97557ef48893b68c333bc1d5ad66c2cf8df4d2 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:23:50 -0400 Subject: [PATCH 08/18] Update user.py Edited refactored code --- App/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/models/user.py b/App/models/user.py index c576f56..a88ca86 100644 --- a/App/models/user.py +++ b/App/models/user.py @@ -19,4 +19,4 @@ def __init__(self, username, password, role="user"): self.password = generate_password_hash(password) def check_password(self, password): - return check_password_hash(self.password, password) + return check_password_hash(self.password, password) From 0636636891784a0ba27123d47538e8ab715fdfe3 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:28:20 -0400 Subject: [PATCH 09/18] Update staff.py From 81879c08bf5966929a416a3f4a340af25c982e1d Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:28:50 -0400 Subject: [PATCH 10/18] Update staff.py From d11a1a267fac65e96f38455ffb750734b454e01c Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:30:17 -0400 Subject: [PATCH 11/18] Update staff.py Made edits to refactored code From ada145c62d81bd58deb327671d1dd91b87e949dc Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Fri, 21 Nov 2025 23:52:46 -0400 Subject: [PATCH 12/18] Update user.py --- App/models/user.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/App/models/user.py b/App/models/user.py index a88ca86..76f43d3 100644 --- a/App/models/user.py +++ b/App/models/user.py @@ -2,6 +2,15 @@ from App.database import db class User(db.Model): + + # Represents a user in the system. + # Attributes: + # ID(int) : PK + # username (string): unique for login + # password (string): hashed password + # role (string): Role of user (staff/admin) + # active_token (string): optional for authentication + id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(20), nullable=False, unique=True) password = db.Column(db.String(256), nullable=False) @@ -13,10 +22,12 @@ class User(db.Model): "polymorphic_on": "role" } - def __init__(self, username, password, role="user"): + def __init__(self, username, password, role="user") -> None: + # Initializes user with username, password and role self.username = username self.role = role self.password = generate_password_hash(password) def check_password(self, password): + # checks if entered password matches the stored hash return check_password_hash(self.password, password) From 92cd822bdd7f1677a01c7660e16e99a48b40ce62 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Sat, 22 Nov 2025 00:01:04 -0400 Subject: [PATCH 13/18] Update staff.py --- App/models/staff.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/App/models/staff.py b/App/models/staff.py index 0a80656..29f6057 100644 --- a/App/models/staff.py +++ b/App/models/staff.py @@ -3,7 +3,11 @@ from datetime import datetime, timedelta class Staff(User): + + # Represents a staff user in system + # Inherits from User and implements staff-specific attributes + #Foreign key referring to User Class id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) __mapper_args__ = { @@ -11,18 +15,19 @@ class Staff(User): } # ---------- Constructor ---------- - def __init__(self, username, password): + def __init__(self, username: str, password: str) -> None: super().__init__(username, password, "staff") + #Staff specific initialisation can be place here in future @property - def upcoming_shifts(self): + def upcoming_shifts(self)_> List: """Return shifts starting after now.""" now = datetime.now() return sorted([s for s in self.shifts if s.start_time > now], key=lambda s: s.start_time) @property - def current_shift(self): - """Return the shift currently in progress.""" + def current_shift(self)-> Optional: + """Return the shift currently in progress, or None if none.""" now = datetime.now() for shift in self.shifts: if shift.start_time <= now <= shift.end_time: @@ -30,7 +35,7 @@ def current_shift(self): return None @property - def total_hours_scheduled(self): + def total_hours_scheduled(self) -> float: """Total hours scheduled across all shifts.""" total = timedelta() for shift in self.shifts: @@ -38,11 +43,11 @@ def total_hours_scheduled(self): return total.total_seconds() / 3600 # convert to hours @property - def completed_shifts(self): + def completed_shifts(self) -> List: return [s for s in self.shifts if s.is_completed] @property - def get_json(self): + def get_json(self)-> Dict: """Return Staff-specific JSON for frontend components.""" return { "id": self.id, From c64d67ca105fce1b0088574a23a63b265c47f546 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Sat, 22 Nov 2025 00:15:17 -0400 Subject: [PATCH 14/18] Update admin.py --- App/models/admin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/App/models/admin.py b/App/models/admin.py index 471b537..1e99935 100644 --- a/App/models/admin.py +++ b/App/models/admin.py @@ -1,27 +1,31 @@ from App.database import db from App.models.user import User - from App.models.strategies.schedule_strategy import ScheduleStrategy 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 +from typing import List, Optional class Admin(User): + + #Represents an admin user on the system + # Inherits from User and Manages staff scheduling using the strategy design pattern + id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) __mapper_args__ = { "polymorphic_identity": "admin", } - def __init__(self, username, password): + def __init__(self, username, password) -> None: super().__init__(username, password, "admin") self.schedule_strategy = None # Strategy pattern methods - def set_schedule_strategy(self, strategy: ScheduleStrategy): + def set_schedule_strategy(self, strategy: ScheduleStrategy)-> None: self.schedule_strategy = strategy - def generate_schedule(self, staff_list, shift_list): + def generate_schedule(self, staff_list, shift_list)-> List: if not self.schedule_strategy: raise ValueError("No strategy assigned") return self.schedule_strategy.generate(staff_list, shift_list) From d3ebb2d4424a1b944bd1e8a105fff4bbe7ac33ad Mon Sep 17 00:00:00 2001 From: kaitlynkhan <160949572+kaitlynkhan@users.noreply.github.com> Date: Sat, 22 Nov 2025 06:30:31 +0000 Subject: [PATCH 15/18] Fix imports and controllers --- App/controllers/__init__.py | 40 ++++++++++++++++--- App/controllers/schedule_controller.py | 3 +- App/main.py | 5 +-- App/models/shift.py | 4 ++ App/models/staff.py | 8 ++-- .../{strategies.py => strategies}/__init__.py | 0 .../balance_day_night.py | 0 .../even_distribution.py | 0 .../minimize_days.py | 0 .../schedule_strategy.py | 0 App/views/user.py | 1 - wsgi.py | 16 ++++---- 12 files changed, 55 insertions(+), 22 deletions(-) rename App/models/{strategies.py => strategies}/__init__.py (100%) rename App/models/{strategies.py => strategies}/balance_day_night.py (100%) rename App/models/{strategies.py => strategies}/even_distribution.py (100%) rename App/models/{strategies.py => strategies}/minimize_days.py (100%) rename App/models/{strategies.py => strategies}/schedule_strategy.py (100%) diff --git a/App/controllers/__init__.py b/App/controllers/__init__.py index 0cb8fd1..22924cf 100644 --- a/App/controllers/__init__.py +++ b/App/controllers/__init__.py @@ -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 + diff --git a/App/controllers/schedule_controller.py b/App/controllers/schedule_controller.py index a729595..62fa330 100644 --- a/App/controllers/schedule_controller.py +++ b/App/controllers/schedule_controller.py @@ -1,8 +1,7 @@ from App.database import db from App.models.schedule import Schedule from App.models.shift import Shift -from App.models.user import Staff, Admin -from App.models.admin import Admin +from App.models import Staff, Admin from datetime import datetime # Import strategies diff --git a/App/main.py b/App/main.py index ee392da..71641f1 100644 --- a/App/main.py +++ b/App/main.py @@ -9,10 +9,7 @@ from App.config import load_config -from App.controllers import ( - setup_jwt, - add_auth_context -) +from App.controllers.auth import setup_jwt, add_auth_context from App.views import views, setup_admin diff --git a/App/models/shift.py b/App/models/shift.py index dcf1649..a640402 100644 --- a/App/models/shift.py +++ b/App/models/shift.py @@ -15,6 +15,10 @@ class Shift(db.Model): start_time = db.Column(db.DateTime, nullable=False) end_time = db.Column(db.DateTime, nullable=False) + #day or night shift + type = db.Column(db.String(10), default="day") + + # Times of clock in or out clock_in = db.Column(db.DateTime, nullable=True) clock_out = db.Column(db.DateTime, nullable=True) diff --git a/App/models/staff.py b/App/models/staff.py index 29f6057..fbbe846 100644 --- a/App/models/staff.py +++ b/App/models/staff.py @@ -1,6 +1,8 @@ from App.database import db from .user import User from datetime import datetime, timedelta +from typing import List, Optional, Dict +from App.models.shift import Shift class Staff(User): @@ -20,13 +22,13 @@ def __init__(self, username: str, password: str) -> None: #Staff specific initialisation can be place here in future @property - def upcoming_shifts(self)_> List: + def upcoming_shifts(self)-> List: """Return shifts starting after now.""" now = datetime.now() return sorted([s for s in self.shifts if s.start_time > now], key=lambda s: s.start_time) @property - def current_shift(self)-> Optional: + def current_shift(self)-> Optional["Shift"]: """Return the shift currently in progress, or None if none.""" now = datetime.now() for shift in self.shifts: @@ -43,7 +45,7 @@ def total_hours_scheduled(self) -> float: return total.total_seconds() / 3600 # convert to hours @property - def completed_shifts(self) -> List: + def completed_shifts(self) -> List["Shift"]: return [s for s in self.shifts if s.is_completed] @property diff --git a/App/models/strategies.py/__init__.py b/App/models/strategies/__init__.py similarity index 100% rename from App/models/strategies.py/__init__.py rename to App/models/strategies/__init__.py diff --git a/App/models/strategies.py/balance_day_night.py b/App/models/strategies/balance_day_night.py similarity index 100% rename from App/models/strategies.py/balance_day_night.py rename to App/models/strategies/balance_day_night.py diff --git a/App/models/strategies.py/even_distribution.py b/App/models/strategies/even_distribution.py similarity index 100% rename from App/models/strategies.py/even_distribution.py rename to App/models/strategies/even_distribution.py diff --git a/App/models/strategies.py/minimize_days.py b/App/models/strategies/minimize_days.py similarity index 100% rename from App/models/strategies.py/minimize_days.py rename to App/models/strategies/minimize_days.py diff --git a/App/models/strategies.py/schedule_strategy.py b/App/models/strategies/schedule_strategy.py similarity index 100% rename from App/models/strategies.py/schedule_strategy.py rename to App/models/strategies/schedule_strategy.py diff --git a/App/views/user.py b/App/views/user.py index 45fbbba..ad50f9e 100644 --- a/App/views/user.py +++ b/App/views/user.py @@ -7,7 +7,6 @@ create_user, get_all_users, get_all_users_json, - jwt_required ) user_views = Blueprint('user_views', __name__, template_folder='../templates') diff --git a/wsgi.py b/wsgi.py index d3cedc3..cd98230 100644 --- a/wsgi.py +++ b/wsgi.py @@ -7,8 +7,8 @@ from App.models import User from App.main import create_app from App.controllers import ( - create_user, get_all_users_json, get_all_users, initialize, - schedule_shift, get_combined_roster, clock_in, clock_out, get_shift_report, login,loginCLI + create_user, get_all_users_json, get_all_users, initialize, add_shift, + get_combined_roster, clock_in, clock_out, get_schedule_report, login,loginCLI ) app = create_app() @@ -75,12 +75,12 @@ def list_user_command(format): @click.argument("schedule_id", type=int) @click.argument("start") @click.argument("end") -def schedule_shift_command(staff_id, schedule_id, start, end): +def add_shift_command(staff_id, schedule_id, start, end): from datetime import datetime admin = require_admin_login() start_time = datetime.fromisoformat(start) end_time = datetime.fromisoformat(end) - shift = schedule_shift(admin.id, staff_id, schedule_id, start_time, end_time) + shift = add_shift(admin.id, staff_id, schedule_id, start_time, end_time) print(f"✅ Shift scheduled under Schedule {schedule_id} by {admin.username}:") print(shift.get_json()) @@ -112,10 +112,12 @@ def clockout_command(shift_id): @shift_cli.command("report", help="Admin views shift report") -def report_command(): +@click.argument("schedule_id", type=int) +def report_command(schedule_id): admin = require_admin_login() - report = get_shift_report(admin.id) - print(f"📊 Shift report for {admin.username}:") + from App.controllers import get_schedule_report + report = get_schedule_report(admin.id, schedule_id) + print(f"📊 Shift report for Schedule {schedule_id}:") print(report) app.cli.add_command(shift_cli) From 3627151d084878a1f3f24eaba2fa82446ee8848a Mon Sep 17 00:00:00 2001 From: r7pt <125412480+r7pt@users.noreply.github.com> Date: Sun, 23 Nov 2025 14:09:18 +0000 Subject: [PATCH 16/18] implemented the Staff views and Admin views --- App/controllers/admin.py | 4 +- App/controllers/auth.py | 14 +-- App/controllers/initialize.py | 34 +++--- App/controllers/schedule_controller.py | 4 +- App/controllers/staff.py | 8 +- App/models/shift.py | 8 +- App/views/adminView.py | 73 +++++++------ App/views/auth.py | 21 ++-- App/views/staffView.py | 138 +++++++++++++++++-------- 9 files changed, 185 insertions(+), 119 deletions(-) diff --git a/App/controllers/admin.py b/App/controllers/admin.py index f1a7db8..9574ed3 100644 --- a/App/controllers/admin.py +++ b/App/controllers/admin.py @@ -18,7 +18,7 @@ def add_shift(admin_id, staff_id, schedule_id, start_time, end_time, shift_type= if not admin or admin.role != "admin": raise PermissionError("Only admins can schedule shifts") - return ScheduleController.add_shift(schedule_id, staff_id, start_time, end_time, shift_type) + return ScheduleController.controller_add_shift(schedule_id, staff_id, start_time, end_time, shift_type) def auto_populate_schedule(admin_id, schedule_id, strategy_name): """Allow an admin to auto-populate shifts using a strategy.""" @@ -34,4 +34,4 @@ def get_schedule_report(admin_id, schedule_id): if not admin or admin.role != "admin": raise PermissionError("Only admins can view schedule reports") - return ScheduleController.get_schedule_report(schedule_id) + return ScheduleController.get_Schedule_report(schedule_id) diff --git a/App/controllers/auth.py b/App/controllers/auth.py index cdce182..a384d32 100644 --- a/App/controllers/auth.py +++ b/App/controllers/auth.py @@ -1,8 +1,9 @@ +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): @@ -12,11 +13,12 @@ def _get_user_by_username(username): def login(username, password): user = _get_user_by_username(username) - if user and user.check_password(password): - return create_access_token(identity=str(user.id)) - - return None + token = create_access_token(identity=username) + response = jsonify(access_token=token) + set_access_cookies(response, token) + return response + return jsonify(message="Invalid username or password"), 401 def loginCLI(username, password): diff --git a/App/controllers/initialize.py b/App/controllers/initialize.py index 49907b2..e82b5d9 100644 --- a/App/controllers/initialize.py +++ b/App/controllers/initialize.py @@ -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 @@ -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) \ No newline at end of file + 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") \ No newline at end of file diff --git a/App/controllers/schedule_controller.py b/App/controllers/schedule_controller.py index 62fa330..b054a66 100644 --- a/App/controllers/schedule_controller.py +++ b/App/controllers/schedule_controller.py @@ -29,7 +29,7 @@ def create_schedule(admin_id, name): return new_schedule @staticmethod - def add_shift(schedule_id, staff_id, start_time, end_time, shift_type="day"): + def controller_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) @@ -77,7 +77,7 @@ def auto_populate(schedule_id, strategy_name): return updated_shifts @staticmethod - def get_schedule_report(schedule_id): + 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: diff --git a/App/controllers/staff.py b/App/controllers/staff.py index f6002cc..4d9cd57 100644 --- a/App/controllers/staff.py +++ b/App/controllers/staff.py @@ -14,7 +14,7 @@ def _assert_staff(staff_id): def _get_shift_for_staff(staff_id, shift_id): """Fetch a shift and verify it belongs to the given staff member.""" - shift = db.session.get(Shift, shift_id) + shift = get_shift(shift_id) if not shift or shift.staff_id != staff_id: raise ValueError("Invalid shift for staff") return shift @@ -44,4 +44,8 @@ def clock_out(staff_id, shift_id): def get_shift(shift_id): - return db.session.get(Shift, shift_id) + shift = Shift.query.get(shift_id) + if not shift: + raise ValueError("Shift not found") + return shift + #return db.session.get(Shift, shift_id) diff --git a/App/models/shift.py b/App/models/shift.py index a640402..51d13e2 100644 --- a/App/models/shift.py +++ b/App/models/shift.py @@ -30,6 +30,13 @@ class Shift(db.Model): lazy=True ) + def __init__(self,staff_id, schedule_id, start_time, end_time) -> None: + self.staff_id = staff_id + self.schedule_id = schedule_id + self.start_time = start_time + self.end_time = end_time + + @property def is_completed(self): return self.clock_in is not None and self.clock_out is not None @@ -62,4 +69,3 @@ def get_json(self): "is_active_shift": self.is_active_shift, "is_late": self.is_late, } - diff --git a/App/views/adminView.py b/App/views/adminView.py index dfbfe76..81b3d0b 100644 --- a/App/views/adminView.py +++ b/App/views/adminView.py @@ -23,53 +23,52 @@ @admin_view.route('/createSchedule', methods=['POST']) @jwt_required() -def createSchedule(): +def admin_createSchedule(): try: - admin_id = get_jwt_identity() + #admin_id= get_jwt_identity() data = request.get_json() - scheduleName = data.get("scheduleName") # gets the scheduleName from the request body - schedule = admin.create_schedule(admin_id, scheduleName) # Call controller method - - return jsonify(schedule.get_json()), 200 # Return the created schedule as JSON - except (PermissionError, ValueError) as e: - return jsonify({"error": str(e)}), 403 - except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 + if data: + schedule = admin.create_schedule(data.get("admin_id"), data.get("name")) + if schedule: + return jsonify(schedule.get_json()), 200 + else: + return jsonify({"error": "Failed to create schedule"}), 500 + except (PermissionError): + return jsonify({"error": "Admin access required"}), 403 -@admin_view.route('/createShift', methods=['POST']) +@admin_view.route('/addShift', methods=['POST']) @jwt_required() -def createShift(): +def admin_add_Shift(): try: - admin_id = get_jwt_identity() + #admin_id = get_jwt_identity() data = request.get_json() - scheduleID = data.get("scheduleID") # gets the scheduleID from the request body - staffID = data.get("staffID") # gets the staffID from the request body - startTime = data.get("start_time") # gets the startTime from the request body - endTime = data.get("end_time") # gets the endTime from the request body - - # Try ISO first, fallback to "YYYY-MM-DD HH:MM:SS" - try: - start_time = datetime.fromisoformat(startTime) - end_time = datetime.fromisoformat(endTime) - except ValueError: - start_time = datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S") - end_time = datetime.strptime(endTime, "%Y-%m-%d %H:%M:%S") - - shift = admin.schedule_shift(admin_id, staffID, scheduleID, start_time, end_time) # Call controller method - print("Debug: Created shift in view:", shift.get_json()) + if not data: + return jsonify({"error": "Invalid input"}), 400 - return jsonify(shift.get_json()), 200 # Return the created shift as JSON - except (PermissionError, ValueError) as e: - return jsonify({"error": str(e)}), 403 - except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 + shift = admin.add_shift( + schedule_id=data.get("schedule_id"), + staff_id=data.get("staff_id"), + start_time=datetime.fromisoformat(data.get("start_time")), + end_time=datetime.fromisoformat(data.get("end_time")), + shift_type=data.get("shift_type", "day"), + admin_id=data.get("admin_id") + ) -@admin_view.route('/shiftReport', methods=['GET']) + if shift: + shiftid= shift.id + updated_shift = admin.auto_populate_schedule(admin_id=data.get("admin_id"),schedule_id=data.get("schedule_id"), strategy_name=data.get("strategy_name", "even_distribution")) + dict_shift = str(updated_shift) + return jsonify(dict_shift), 200 + except (PermissionError, ValueError) as e: + return jsonify({"error": str(e)}), 500 + +@admin_view.route('/scheduleReport', methods=['GET']) @jwt_required() -def shiftReport(): +def scheduleReport(): try: - admin_id = get_jwt_identity() - report = admin.get_shift_report(admin_id) # Call controller method + #admin_id = get_jwt_identity() + data = request.get_json() + report = admin.get_schedule_report(data['admin_id'], data['schedule_id']) return jsonify(report), 200 except (PermissionError, ValueError) as e: return jsonify({"error": str(e)}), 403 diff --git a/App/views/auth.py b/App/views/auth.py index dfc4dc9..cc4a788 100644 --- a/App/views/auth.py +++ b/App/views/auth.py @@ -1,8 +1,9 @@ from flask import Blueprint, render_template, jsonify, request, flash, send_from_directory, flash, redirect, url_for -from flask_jwt_extended import jwt_required, current_user, unset_jwt_cookies, set_access_cookies +from flask_jwt_extended import get_jwt_identity, jwt_required, current_user, unset_jwt_cookies, set_access_cookies, create_access_token +from App.models import User +from App.controllers.auth import login - -from.index import index_views +from.index import index_views from App.controllers import ( login, @@ -35,6 +36,7 @@ def login_action(): flash('Login Successful') set_access_cookies(response, token) return response + @auth_views.route('/logout', methods=['GET']) def logout_action(): @@ -50,17 +52,18 @@ def logout_action(): @auth_views.route('/api/login', methods=['POST']) def user_login_api(): data = request.json - token = login(data['username'], data['password']) - if not token: - return jsonify(message='bad username or password given'), 401 - response = jsonify(access_token=token) - set_access_cookies(response, token) + response = login(data['username'], data['password']) + if not response: + return jsonify(message='bad username or password given'), 403 return response @auth_views.route('/api/identify', methods=['GET']) @jwt_required() def identify_user(): - return jsonify({'message': f"username: {current_user.username}, id : {current_user.id}"}) + return jsonify( + #username=current_user.username, + id=current_user.id + ), 200 @auth_views.route('/api/logout', methods=['GET']) def logout_api(): diff --git a/App/views/staffView.py b/App/views/staffView.py index d9a9f47..5e3dda1 100644 --- a/App/views/staffView.py +++ b/App/views/staffView.py @@ -1,6 +1,6 @@ # app/views/staff_views.py from flask import Blueprint, jsonify, request -from App.controllers import staff, auth +from App.controllers import staff, auth,user, get_all_users from flask_jwt_extended import jwt_required, get_jwt_identity from sqlalchemy.exc import SQLAlchemyError @@ -14,58 +14,106 @@ staff_views = Blueprint('staff_views', __name__, template_folder='../templates') -# Staff view roster route -@staff_views.route('/staff/roster', methods=['GET']) -@jwt_required() -def view_roster(): - try: - staff_id = get_jwt_identity() # get the user id stored in JWT - # staffData = staff.get_user(staff_id).get_json() # Fetch staff data - roster = staff.get_combined_roster(staff_id) # staff.get_combined_roster should return the json data of the roseter - return jsonify(roster), 200 - except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 +@staff_views.route("/allshifts", methods=['GET']) +def get_all_shifts(): + data = request.get_json() + staffID = data.get("staff_id") + staf = staff._assert_staff(staffID) + if not staffID or not staf: + return jsonify({"error": "Unauthorized access"}), 403 + shifts = staff.get_combined_roster(staffID) + return jsonify(shifts), 200 + -@staff_views.route('/staff/shift', methods=['GET']) +#get shift for staff +@staff_views.route('/staffshift', methods=['GET']) @jwt_required() -def view_shift(): +def staff_get_shift(): try: - data = request.get_json() - shift_id = data.get("shiftID") # gets the shiftID from the request - shift = staff.get_shift(shift_id) # Call controller - if not shift: - return jsonify({"error": "Shift not found"}), 404 + data=request.get_json() + shiftID = data.get("shift_id") + staffID = data.get("staff_id") + if not shiftID or not staffID: + return jsonify({"error": "valid shift_id and staff_id are required"}), 400 + staf = staff._assert_staff(staffID) + #if staffID != int(get_jwt_identity()) or not staf: + if not staffID or not staf: + return jsonify({"error": "Unauthorized access"}), 403 + shift = staff._get_shift_for_staff(int(staffID), int(shiftID)) return jsonify(shift.get_json()), 200 - except SQLAlchemyError: + except(SQLAlchemyError) as e: return jsonify({"error": "Database error"}), 500 + except(ValueError) as ve: + return jsonify({"error": str(ve)}), 404 + -# Staff Clock in endpoint -@staff_views.route('/staff/clock_in', methods=['POST']) +@staff_views.route('/staff/combinedRoster', methods=['GET']) @jwt_required() -def clockIn(): +def get_combinedRoster(): try: - staff_id = int(get_jwt_identity())# db uses int for userID so we must convert - data = request.get_json() - shift_id = data.get("shiftID") # gets the shiftID from the request - shiftOBJ = staff.clock_in(staff_id, shift_id) # Call controller - return jsonify(shiftOBJ.get_json()), 200 - except (PermissionError, ValueError) as e: - return jsonify({"error": str(e)}), 403 - except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 + #staffId =int(get_jwt_identity()) + data=request.get_json() + #shiftID = data.get("shift_id") + staffId = data.get("staff_id") + staf = staff._assert_staff(staffId) + if staf: + roster = staff.get_combined_roster(staffId) + if not roster: + return jsonify({"error": "no roster found"}), 404 + return jsonify(roster), 200 + else: + return jsonify({"error": "unauthorized access"}), 403 + except(SQLAlchemyError) as e: + return jsonify({"error": "database error"}), 500 +@staff_views.route("/staff/clockIn", methods=["POST"]) +@jwt_required() +def staff_clock_in(): + #staffId=int(get_jwt_identity()) + data=request.get_json() + staffId = data.get("staff_id") + staf = staff._assert_staff(staffId) + if not staf: + return jsonify({"error": "unauthorized access"}), 403 + currentshift = staf.current_shift + if currentshift is None: + return jsonify({"error": " not currrent shift found "}), 404 + shiftid = currentshift.id + currentshift = staff.clock_in(staffId,4) + return jsonify(currentshift), 200 -# Staff Clock in endpoint -@staff_views.route('/staff/clock_out/', methods=['POST']) +@staff_views.route("/staff/clockOut", methods=["POST"]) @jwt_required() -def clock_out(): - try: - staff_id = int(get_jwt_identity()) # db uses int for userID so we must convert - data = request.get_json() - shift_id = data.get("shiftID") # gets the shiftID from the request - shift = staff.clock_out(staff_id, shift_id) # Call controller - return jsonify(shift.get_json()), 200 - except (PermissionError, ValueError) as e: - return jsonify({"error": str(e)}), 403 - except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 \ No newline at end of file +def staff_clock_out(): + staffId=int(get_jwt_identity()) + staff= staff._assert_staff(staffId) + if not staff: + return jsonify({"error": "unauthorized access"}), 403 + current_shift = staff.current_shift + if not current_shift: + return jsonify({"error": " not currrent shift found "}), 404 + shiftid = current_shift.id + current_shift = staff.clock_out(staffId,shiftid) + return jsonify(current_shift), 200 + + + + + + + + + + + + + + + + + + + + + + From c74595e1bdf6f5d1672192a9116d551f0516c320 Mon Sep 17 00:00:00 2001 From: tlbuwi01 Date: Sun, 23 Nov 2025 22:22:04 -0400 Subject: [PATCH 17/18] Unit Tests Implementation --- App/tests/test_app.py | 370 +++++++++++++++++++----------------------- 1 file changed, 165 insertions(+), 205 deletions(-) diff --git a/App/tests/test_app.py b/App/tests/test_app.py index e52b6a5..b1bc7ed 100644 --- a/App/tests/test_app.py +++ b/App/tests/test_app.py @@ -3,241 +3,201 @@ from App.main import create_app from App.database import db, create_db from datetime import datetime, timedelta -from App.models import User, Schedule, Shift -from App.controllers import ( - create_user, - get_all_users_json, - loginCLI, - get_user, - update_user, - schedule_shift, - get_shift_report, - get_combined_roster, - clock_in, - clock_out, - get_shift -) +#modelz +from App.models import User, Staff, Admin, Schedule, Shift +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 +#controllerz +from App.controllers.user import create_user, get_user, update_user, get_all_users_json +from App.controllers.staff import clock_in, clock_out, get_combiner_roster +from App.controllers.admin import get_schedule_report +from App.controllers.schedule_controller import ScheduleController, schedule_shift, get_shift_report +from App.controllers.shift_controller import get_shift +@pytest.fixture(autouse=True) +def clean_db(): + db.drop_all() + create_db() + db.session.remove() + yield + +@pytest.fixture(autouse= True, scope="module") +def empty_db(): + app= create_app({'TESTING': True, 'SQLALCHEMY_DATABASE_URI': 'sqlite://test.db'}) + create_db() + db.session.remove() + yield app.test_client() + db.drop_all() LOGGER = logging.getLogger(__name__) -''' - Unit Tests -''' +### User unit tests ### class UserUnitTests(unittest.TestCase): -# User unit tests - def test_new_user_admin(self): - user = create_user("bot", "bobpass","admin") - assert user.username == "bot" - - def test_new_user_staff(self): - user = create_user("pam", "pampass","staff") - assert user.username == "pam" + def test_create_user_valid(self): + user= user_controller.create_user ("bob", "pass123", "user") + self.assertEqual(user.username, "bob") + self.assertEqual(user.role, "user") + self.assertTrue(user.check_password("pass123")) def test_create_user_invalid_role(self): - user = create_user("jim", "jimpass","ceo") - assert user == None + user = user_controller.create_user("bob", "pass123", "ceo") + self.assertIsNone(user) + def test_check_password_correct(self): + user= create_user("alice", "pass123", "user") + self.assertTrue (user.chech_password("pass123") + def test_check_password_incorrect(self): + user= create_user("alice2", "pass123", "user") + self.assertFalse(user.check_password("wrongpassword")) + def test_get_json(self): - user = User("bob", "bobpass", "admin") - user_json = user.get_json() - self.assertDictEqual(user_json, {"id":None, "username":"bob", "role":"admin"}) + user = create_user("charlie", "pass123", "user") + user_json= user.get_json() + self.assertEqual(user.get_json["username"], "charlie") + self.assertEqual(user_json["role"],"user") + + def test_update_username(self): + user = create_user("dave", "pass123", "user") + update_user (user.id, "newname") + updates = get_user(user.id) + self.assertEqual (updated.username, "newname") - def test_hashed_password(self): - password = "mypass" - user = User(username="tester", password=password) - assert user.password != password - assert user.check_password(password) is True - - def test_check_password(self): - password = "mypass" - user = User("bob", password) - assert user.check_password(password) -# Admin unit tests - def test_schedule_shift_valid(self): - admin = create_user("admin1", "adminpass", "admin") - staff = create_user("staff1", "staffpass", "staff") - schedule = Schedule(name="Morning Schedule", created_by=admin.id) - db.session.add(schedule) - db.session.commit() - - start = datetime(2025, 10, 22, 8, 0, 0) - end = datetime(2025, 10, 22, 16, 0, 0) - - shift = schedule_shift(admin.id, staff.id, schedule.id, start, end) - - assert shift.staff_id == staff.id - assert shift.schedule_id == schedule.id - assert shift.start_time == start - assert shift.end_time == end - assert isinstance(shift, Shift) - - def test_schedule_shift_invalid(self): - admin = User("admin2", "adminpass", "admin") - staff = User("staff2", "staffpass", "staff") - invalid_schedule_id = 999 - - start = datetime(2025, 10, 22, 8, 0, 0) - end = datetime(2025, 10, 22, 16, 0, 0) - try: - shift = schedule_shift(admin.id, staff.id, invalid_schedule_id, start, end) - assert shift is None - except Exception: - assert True - - def test_get_shift_report(self): - admin = create_user("superadmin", "superpass", "admin") - staff = create_user("worker1", "workerpass", "staff") - db.session.add_all([admin, staff]) - db.session.commit() - - schedule = Schedule(name="Weekend Schedule", created_by=admin.id) - db.session.add(schedule) - db.session.commit() - - shift1 = schedule_shift(admin.id, staff.id, schedule.id, - datetime(2025, 10, 26, 8, 0, 0), - datetime(2025, 10, 26, 16, 0, 0)) - shift2 = schedule_shift(admin.id, staff.id, schedule.id, - datetime(2025, 10, 27, 8, 0, 0), - datetime(2025, 10, 27, 16, 0, 0)) +### Admin unit test ### + +class AdminUnitTests(unittest.TestCase): + + def test_create_staff(self): + staff = staff_controller.create_staff ("Mrs.Jane") + self.assertEqual (staff.username, "Mrs.Jane") + + def test_create_staff_empty_name(self): + result = staff_controller.create_staff("") + self.assertEqual(result, {"error": "staff name cannot be empty."}) + + def test_create_staff_duplicate(self): + staff_controller.create_stafff("Mrs.Jane") + result = staff_controller.create_staff("Mrs.Jane") + self.assertEqual(result, {"error": "Staff with name 'Mrs.Jane' already exists."}) + + def test_confirm_hours_valid(self): + #mock setup + staff= staff_controller.create_staff("StaffA") + hours_id = 1 + result = staff_controller.confirm_hours(hours_id, staff.id) + self.assertEqual(result, "message": f"Hours record {hours_id} confirmed by staff {staff.id}."}) + + def test_confirm_hours_invalid(self): + hours_id=1 + result= staff_controller.confirm_hours(hours_id,"abc") + self.assertEqual(result, {"error": "Staff member with IS 'abc' not found."}) - report = get_shift_report(admin.id) - assert len(report) >= 2 - assert report[0]["staff_id"] == staff.id - assert report[0]["schedule_id"] == schedule.id - - def test_get_shift_report_invalid(self): - non_admin = User("randomstaff", "randompass", "staff") - - try: - get_shift_report(non_admin.id) - assert False, "Expected PermissionError for non-admin user" - except PermissionError as e: - assert str(e) == "Only admins can view shift reports" -# Staff unit tests - def test_get_combined_roster_valid(self): - staff = create_user("staff3", "pass123", "staff") - admin = create_user("admin3", "adminpass", "admin") - schedule = Schedule(name="Test Schedule", created_by=admin.id) - db.session.add(schedule) - db.session.commit() + def test_confirm_hours_invalid_record(self): + result = staff_controller.confirm_hours("abc", 3) + self.assertEqual (result, {"error": "Invalid ID format providied"}) - # create a shift - shift = schedule_shift(admin.id, staff.id, schedule.id, - datetime(2025, 10, 23, 8, 0, 0), - datetime(2025, 10, 23, 16, 0, 0)) + def test_confirm_hours_previous_confirmed(self): + #done assuming that hours_id=5 is already confirmed by staff 8 + result = staff_controller.confirm_hours(5,9) + self.assertEqual(result, {"message": "Hours record 5 was already confirmed by staff ID 8"}) - roster = get_combined_roster(staff.id) - assert len(roster) >= 1 - assert roster[0]["staff_id"] == staff.id - assert roster[0]["schedule_id"] == schedule.id - - def test_get_combined_roster_invalid(self): - non_staff = create_user("admin4", "adminpass", "admin") - try: - get_combined_roster(non_staff.id) - assert False, "Expected PermissionError for non-staff" - except PermissionError as e: - assert str(e) == "Only staff can view roster" + +### Staff unit tests ### + +class StaffUnitTests(unittest.TestCase): + + def test_staff_creation_valid(self): + staff= Staff ("john", "pass123") + self.assertEqual(staff.role, "staff") + self.assertTrue(staff.check_password("pass123")) + + def test_staff_upcoming_shifts(self): + staff= Staff ("alice", "pass123") + shift1= Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now()+timedelta(hours=1), end_time=datetime.now()+timedelta(hours=3)) + shift2= Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now()+timedelta(hours=2), end_time=datetime.now()+timedelta(hours=4)) + staff.shifts = [shift2, shift1] + self.assertEqual (staff.upcoming_shifts, sorted(staff.shofts, key= lambda s: start_time)) + + def test_staff_current_shift(self): + staff = Staff ("bob" , "pass123") + now = datetime.now() + active_shift = Shift (staff_id= staff.id, schedule_id=1, start_time=now - timedelta(hours=1), end_time=now + timedelta(hours=1)) + staff.shifts= [active_shift] + self.assertEqual(staff.current_shift, active_shift) + + def test_staff_total_hours_scheduled(self): + staff = Staff("charlie", "pass123") + shift1= Shift (staff_id= staff.id, schedule_id=1, start_time=datetime.now(), end_time= datetime.now() + timedelta(hours=1)) + shift2= Shift (staff_id= staff.id, schedule_id=1, start_time=datetime.now(), end_time= datetime.now() + timedelta(hours=1)) + staff.shifts= [shift1, shift2] + self.assertAlmostEqual(staff.total_hours_schedules,5) + + def test_staff_completed_shifts(self): + satff = Staff("dana", "pass123") + shift1= Shift (staff_id= staff.id, schedule_id=1, start_time=datetime.now(), end_time= datetime.now() + timedelta(hours=1)) + shift2= Shift (staff_id= staff.id, schedule_id=1, start_time=datetime.now(), end_time= datetime.now() + timedelta(hours=1)) + shift1.clock_in= datetime.now() + shift1.clock_out = datetime.now() +timedelta(hours=1) + shift2.clock_in = datetime.now() + staff.shifts= [shift1, shift2] + self.assertEqual (staff.completed_shifts, [shift1]) + + def test_get_json_staff(self): + staff = Staff("emma", "pass123") + shift1 = Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + staff.shifts = [shift1] + json_data = staff.get_json() + self.assertEqual(json_data["username"], "emma") + self.assertEqual(json_data["role"], "staff") + self.assertEqual(json_data["upcoming_shift_count"], len(staff.upcoming_shifts)) def test_clock_in_valid(self): - admin = create_user("admin_clock", "adminpass", "admin") - staff = create_user("staff_clock", "staffpass", "staff") - - schedule = Schedule(name="Clock Schedule", created_by=admin.id) - db.session.add(schedule) + staff = Staff("frank", "pass123") + shift = Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + db.session.add(shift) db.session.commit() - - start = datetime(2025, 10, 25, 8, 0, 0) - end = datetime(2025, 10, 25, 16, 0, 0) - shift = schedule_shift(admin.id, staff.id, schedule.id, start, end) - - clocked_in_shift = clock_in(staff.id, shift.id) - assert clocked_in_shift.clock_in is not None - assert isinstance(clocked_in_shift.clock_in, datetime) - - def test_clock_in_invalid_user(self): - admin = create_user("admin_clockin", "adminpass", "admin") - schedule = Schedule(name="Invalid Clock In", created_by=admin.id) - db.session.add(schedule) - db.session.commit() - - staff = create_user("staff_invalid", "staffpass", "staff") - start = datetime(2025, 10, 26, 8, 0, 0) - end = datetime(2025, 10, 26, 16, 0, 0) - shift = schedule_shift(admin.id, staff.id, schedule.id, start, end) - - with pytest.raises(PermissionError) as e: - clock_in(admin.id, shift.id) - assert str(e.value) == "Only staff can clock in" + shift = staff_controller.clock_in(staff.id, shift.id) + self.assertIsNotNone(shift.clock_in) def test_clock_in_invalid_shift(self): - staff = create_user("clockstaff_invalid", "clockpass", "staff") - with pytest.raises(ValueError) as e: - clock_in(staff.id, 999) - assert str(e.value) == "Invalid shift for staff" + staff = Staff("george", "pass123") + with self.assertRaises(ValueError): + staff_controller.clock_in(staff.id, 999) def test_clock_out_valid(self): - admin = create_user("admin_clockout", "adminpass", "admin") - staff = create_user("staff_clockout", "staffpass", "staff") - - schedule = Schedule(name="ClockOut Schedule", created_by=admin.id) - db.session.add(schedule) + staff = Staff("harry", "pass123") + shift = Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + db.session.add(shift) db.session.commit() - - start = datetime(2025, 10, 27, 8, 0, 0) - end = datetime(2025, 10, 27, 16, 0, 0) - shift = schedule_shift(admin.id, staff.id, schedule.id, start, end) - - clocked_out_shift = clock_out(staff.id, shift.id) - assert clocked_out_shift.clock_out is not None - assert isinstance(clocked_out_shift.clock_out, datetime) - - def test_clock_out_invalid_user(self): - admin = create_user("admin_invalid_out", "adminpass", "admin") - schedule = Schedule(name="Invalid ClockOut Schedule", created_by=admin.id) - db.session.add(schedule) - db.session.commit() - - staff = create_user("staff_invalid_out", "staffpass", "staff") - start = datetime(2025, 10, 28, 8, 0, 0) - end = datetime(2025, 10, 28, 16, 0, 0) - shift = schedule_shift(admin.id, staff.id, schedule.id, start, end) - - with pytest.raises(PermissionError) as e: - clock_out(admin.id, shift.id) - assert str(e.value) == "Only staff can clock out" + shift = staff_controller.clock_out(staff.id, shift.id) + self.assertIsNotNone(shift.clock_out) def test_clock_out_invalid_shift(self): - staff = create_user("staff_invalid_shift_out", "staffpass", "staff") - with pytest.raises(ValueError) as e: - clock_out(staff.id, 999) - assert str(e.value) == "Invalid shift for staff" + staff = Staff("ivan", "pass123") + with self.assertRaises(ValueError): + staff_controller.clock_out(staff.id, 999) + + def test_combined_roster(self): + staff = Staff("jack", "pass123") + shift1 = Shift(staff_id=staff.id, schedule_id=1, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + shift2 = Shift(staff_id=staff.id, schedule_id=2, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + staff.shifts = [shift1, shift2] + roster = staff_controller.get_combined_roster(staff.id) + self.assertEqual(len(roster), 2) + + def test_staff_permission_block(self): + non_staff = create_user("kelly", "pass123", "user") + shift = Shift(staff_id=1, schedule_id=1, start_time=datetime.now(), end_time=datetime.now() + timedelta(hours=2)) + with self.assertRaises(PermissionError): + staff_controller.clock_in(non_staff.id, shift.id) ''' Integration Tests ''' -@pytest.fixture(autouse=True) -def clean_db(): - db.drop_all() - create_db() - db.session.remove() - yield -# This fixture creates an empty database for the test and deletes it after the test -# scope="class" would execute the fixture once and resued for all methods in the class -@pytest.fixture(autouse=True, scope="module") -def empty_db(): - app = create_app({'TESTING': True, 'SQLALCHEMY_DATABASE_URI': 'sqlite:///test.db'}) - create_db() - db.session.remove() - yield app.test_client() - db.drop_all() - def test_authenticate(): user = User("bob", "bobpass","user") @@ -368,4 +328,4 @@ def test_permission_restrictions(self): get_combined_roster(admin.id) with self.assertRaises(PermissionError): - get_shift_report(staff.id) \ No newline at end of file + get_shift_report(staff.id) From a580507b861b6d3142afa2f3d119847b1364a7f6 Mon Sep 17 00:00:00 2001 From: r7pt <125412480+r7pt@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:44:58 +0000 Subject: [PATCH 18/18] API errors fixed --- App/config.py | 2 +- App/controllers/auth.py | 2 +- App/controllers/staff.py | 5 +---- App/views/auth.py | 20 +++++++------------- App/views/staffView.py | 17 +++++++---------- 5 files changed, 17 insertions(+), 29 deletions(-) diff --git a/App/config.py b/App/config.py index c34caff..2665f83 100644 --- a/App/config.py +++ b/App/config.py @@ -12,7 +12,7 @@ def load_config(app, overrides): app.config['UPLOADED_PHOTOS_DEST'] = "App/uploads" app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_token' app.config["JWT_TOKEN_LOCATION"] = ["cookies", "headers"] - app.config["JWT_COOKIE_SECURE"] = True + app.config["JWT_COOKIE_SECURE"] = False app.config["JWT_COOKIE_CSRF_PROTECT"] = False app.config['FLASK_ADMIN_SWATCH'] = 'darkly' for key in overrides: diff --git a/App/controllers/auth.py b/App/controllers/auth.py index a384d32..c7073dc 100644 --- a/App/controllers/auth.py +++ b/App/controllers/auth.py @@ -14,7 +14,7 @@ def _get_user_by_username(username): def login(username, password): user = _get_user_by_username(username) if user and user.check_password(password): - token = create_access_token(identity=username) + token = create_access_token(identity=user) response = jsonify(access_token=token) set_access_cookies(response, token) return response diff --git a/App/controllers/staff.py b/App/controllers/staff.py index 4d9cd57..aea697d 100644 --- a/App/controllers/staff.py +++ b/App/controllers/staff.py @@ -28,7 +28,6 @@ def get_combined_roster(staff_id): def clock_in(staff_id, shift_id): _assert_staff(staff_id) shift = _get_shift_for_staff(staff_id, shift_id) - shift.clock_in = datetime.now() db.session.commit() return shift @@ -37,15 +36,13 @@ def clock_in(staff_id, shift_id): def clock_out(staff_id, shift_id): _assert_staff(staff_id) shift = _get_shift_for_staff(staff_id, shift_id) - shift.clock_out = datetime.now() db.session.commit() return shift def get_shift(shift_id): - shift = Shift.query.get(shift_id) + shift = db.session.get(Shift, shift_id) if not shift: raise ValueError("Shift not found") return shift - #return db.session.get(Shift, shift_id) diff --git a/App/views/auth.py b/App/views/auth.py index cc4a788..313eb48 100644 --- a/App/views/auth.py +++ b/App/views/auth.py @@ -27,15 +27,11 @@ def identify_page(): @auth_views.route('/login', methods=['POST']) def login_action(): - data = request.form - token = login(data['username'], data['password']) - response = redirect(request.referrer) - if not token: - flash('Bad username or password given'), 401 - else: - flash('Login Successful') - set_access_cookies(response, token) - return response + data = request.json + response = login_user(data['username'], data['password']) + if not response: + return jsonify(message='bad username or password given'), 403 + return response @auth_views.route('/logout', methods=['GET']) @@ -60,10 +56,8 @@ def user_login_api(): @auth_views.route('/api/identify', methods=['GET']) @jwt_required() def identify_user(): - return jsonify( - #username=current_user.username, - id=current_user.id - ), 200 + username = get_jwt_identity() + return jsonify(logged_in_as=username), 200 @auth_views.route('/api/logout', methods=['GET']) def logout_api(): diff --git a/App/views/staffView.py b/App/views/staffView.py index 5e3dda1..112c47b 100644 --- a/App/views/staffView.py +++ b/App/views/staffView.py @@ -17,7 +17,7 @@ @staff_views.route("/allshifts", methods=['GET']) def get_all_shifts(): data = request.get_json() - staffID = data.get("staff_id") + staffId =int(get_jwt_identity()) staf = staff._assert_staff(staffID) if not staffID or not staf: return jsonify({"error": "Unauthorized access"}), 403 @@ -29,14 +29,15 @@ def get_all_shifts(): @staff_views.route('/staffshift', methods=['GET']) @jwt_required() def staff_get_shift(): - try: + try: + staffID =int(get_jwt_identity()) + data=request.get_json() shiftID = data.get("shift_id") - staffID = data.get("staff_id") + #staffID = data.get("staff_id") if not shiftID or not staffID: return jsonify({"error": "valid shift_id and staff_id are required"}), 400 staf = staff._assert_staff(staffID) - #if staffID != int(get_jwt_identity()) or not staf: if not staffID or not staf: return jsonify({"error": "Unauthorized access"}), 403 shift = staff._get_shift_for_staff(int(staffID), int(shiftID)) @@ -51,10 +52,8 @@ def staff_get_shift(): @jwt_required() def get_combinedRoster(): try: - #staffId =int(get_jwt_identity()) + staffId =int(get_jwt_identity()) data=request.get_json() - #shiftID = data.get("shift_id") - staffId = data.get("staff_id") staf = staff._assert_staff(staffId) if staf: roster = staff.get_combined_roster(staffId) @@ -69,9 +68,7 @@ def get_combinedRoster(): @staff_views.route("/staff/clockIn", methods=["POST"]) @jwt_required() def staff_clock_in(): - #staffId=int(get_jwt_identity()) - data=request.get_json() - staffId = data.get("staff_id") + staffId=int(get_jwt_identity()) staf = staff._assert_staff(staffId) if not staf: return jsonify({"error": "unauthorized access"}), 403