diff --git a/app/db/crud.py b/app/db/crud.py index eba10b7..55d0aa5 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -1,7 +1,7 @@ from typing import Optional from sqlalchemy.orm import Session from app.db import models -from datetime import datetime, date +from datetime import datetime, date, timedelta from sqlalchemy import func def get_user_by_email(db: Session, email: str): @@ -10,6 +10,7 @@ def get_user_by_email(db: Session, email: str): def get_task(db: Session, track_id: int, task_no: int): return db.query(models.Task).filter_by(track_id=track_id, task_no=task_no).first() + def submit_task(db: Session, mentee_id: int, task_id: int, reference_link: str,start_date: date): existing = db.query(models.Submission).filter_by(mentee_id=mentee_id, task_id=task_id).first() if existing: @@ -30,15 +31,16 @@ def submit_task(db: Session, mentee_id: int, task_id: int, reference_link: str,s db.refresh(submission) return submission -def approve_submission(db: Session, submission_id: int, mentor_feedback: str, status: str): +def approve_submission(db: Session, submission_id: int, mentor_feedback: str, points_awarded: int): sub = db.query(models.Submission).filter_by(id=submission_id).first() if not sub: return None - sub.status = status sub.mentor_feedback = mentor_feedback - if status == "approved": - sub.approved_at = datetime.utcnow() + sub.approved_at = datetime.now() + sub.points_awarded += points_awarded + sub.total_paused_time = (datetime.now() - sub.submitted_at).days + sub.status = "approved" db.commit() db.refresh(sub) @@ -76,6 +78,46 @@ def create_or_update_otp(db, email, otp, expires_at): db.add(entry) db.commit() +def pause_task(db: Session, submission: int): + pause_row=db.query(models.Submission).filter_by(id=submission).first() + pause_row.pause_start=datetime.now() + pause_row.status = "paused" + db.commit() + db.refresh(pause_row) + return pause_row + +def end_pause(db: Session, submission: int): + pause_row=db.query(models.Submission).filter_by(id=submission).first() + pause_row.total_paused_time+= (datetime.now().date() - pause_row.pause_start.date()).days + pause_row.status = "ongoing" + pause_row.pause_start = None + db.commit() + db.refresh(pause_row) + return pause_row + +def start_task(db: Session, task_id: int, mentee_id: int): + task_start = models.Submission( + mentee_id = mentee_id, + task_id = task_id, + start_date = datetime.now(), + status = "ongoing" + ) + db.add(task_start) + db.commit() + db.refresh(task_start) + return task_start + +def find_time_spent_on_task(db: Session, submission_id: int): + submission = db.query(models.Submission).filter_by(id=submission_id).first() + start = datetime.fromisoformat(submission.start_date) if isinstance(submission.start_date, str) else submission.start_date + end = submission.submitted_at or datetime.now() + time_spent = (end - start - timedelta(days=submission.total_paused_time)).days + return time_spent + +def get_submission(db: Session, mentee_email: str, track_id: int, task_no: int): + mentee = get_user_by_email(db, email=mentee_email) + task = get_task(db, track_id=track_id, task_no=task_no) + return db.query(models.Submission).filter_by(mentee_id=mentee.id, task_id=task.id).first() def get_submissions(db: Session, email: str, track_id: Optional[int] = None): user = db.query(models.User).filter(models.User.email == email).first() @@ -85,4 +127,20 @@ def get_submissions(db: Session, email: str, track_id: Optional[int] = None): if track_id is not None: query = query.join(models.Task, models.Submission.task_id == models.Task.id) query = query.filter(models.Task.track_id == track_id) - return query.all() \ No newline at end of file + return query.all() + +def reject_submission(db: Session, submission_id: int, mentor_feedback: str): + sub = db.query(models.Submission).filter_by(id=submission_id).first() + if not sub: + return None + + sub.reference_link = None + sub.mentor_feedback = mentor_feedback + sub.total_paused_time = (datetime.now() - sub.submitted_at).days + sub.submitted_at = None + sub.status = "ongoing" + sub.start_date = None + + db.commit() + db.refresh(sub) + return sub \ No newline at end of file diff --git a/app/db/models.py b/app/db/models.py index 4c1df21..641e242 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,6 +1,5 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint from sqlalchemy.orm import relationship -from datetime import datetime from app.db.db import Base class User(Base): @@ -37,12 +36,16 @@ class Submission(Base): id = Column(Integer, primary_key=True, index=True) mentee_id = Column(Integer, ForeignKey("users.id"), nullable=False) task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) - reference_link = Column(Text, nullable=False) - status = Column(String, default="submitted") # submitted / approved / paused / rejected - submitted_at = Column(DateTime, default=datetime.utcnow) - start_date = Column(DateTime, nullable=False) + + reference_link = Column(Text, nullable=True) + status = Column(String, default="ongoing") # submitted / approved / paused / ongoing / not started + start_date = Column(String, nullable=False) + submitted_at = Column(DateTime, nullable=True) approved_at = Column(DateTime, nullable=True) mentor_feedback = Column(Text, nullable=True) + pause_start = Column(DateTime, nullable=True) + total_paused_time = Column(Integer, nullable=False, default=0) + mentee = relationship("User") task = relationship("Task") @@ -63,7 +66,7 @@ class LeaderboardEntry(Base): tasks_completed = Column(Integer, default=0) class OTP(Base): __tablename__ = "otp" - + email = Column(String, primary_key=True, index=True) otp = Column(String, nullable=False) expires_at = Column(DateTime, nullable=False) \ No newline at end of file diff --git a/app/routes/progress.py b/app/routes/progress.py index 0130b93..dc0f6c6 100644 --- a/app/routes/progress.py +++ b/app/routes/progress.py @@ -2,14 +2,14 @@ from sqlalchemy.orm import Session from app.db import crud, models from app.db.db import get_db -from app.schemas.submission import SubmissionCreate, SubmissionOut, SubmissionApproval +from app.schemas.submission import SubmissionCreate, SubmissionOut, SubmissionApproval, PauseTask, StartTask router = APIRouter() -@router.post("/submit-task", response_model=SubmissionOut) +@router.post("/submit-task", response_model=SubmissionOut) # check if it has already been submitted, def submit_task(data: SubmissionCreate, db: Session = Depends(get_db)): # 1. Validate mentee - mentee = crud.get_user_by_email(db, data.mentee_email) + mentee = crud.get_user_by_email(db, email=data.mentee_email) if not mentee or mentee.role != "mentee": raise HTTPException(status_code=403, detail="Invalid or missing mentee") @@ -18,17 +18,20 @@ def submit_task(data: SubmissionCreate, db: Session = Depends(get_db)): if not task: raise HTTPException(status_code=404, detail="Task not found") + # 3. Submit submission = crud.submit_task(db, mentee_id=mentee.id, task_id=task.id, reference_link=data.reference_link, start_date=data.start_date) if not submission: raise HTTPException(status_code=400, detail="Task already submitted") + # 4. Submit task + submission = crud.submit_task(db, mentee_id=mentee.id, task_id=task.id, reference_link=data.reference_link, status = "submitted") return submission -@router.patch("/approve-task", response_model=SubmissionOut) +@router.patch("/review-task", response_model=SubmissionOut) def approve_task(data: SubmissionApproval, db: Session = Depends(get_db)): # 1. Validate mentor - mentor = crud.get_user_by_email(db, data.mentor_email) + mentor = crud.get_user_by_email(db, email=data.mentor_email) if not mentor or mentor.role != "mentor": raise HTTPException(status_code=403, detail="Invalid or missing mentor") @@ -36,16 +39,68 @@ def approve_task(data: SubmissionApproval, db: Session = Depends(get_db)): sub = db.query(models.Submission).filter_by(id=data.submission_id).first() if not sub: raise HTTPException(status_code=404, detail="Submission not found") + task = db.query(models.Tasks).filter_by(id=sub.task_id).first() # 3. Confirm mentor is assigned to this mentee if not crud.is_mentor_of(db, mentor.id, sub.mentee_id): raise HTTPException(status_code=403, detail="Mentor not authorized for this mentee") + + # 4. See if accepted or rejected + if data.accepted: + if data.points_awarded > task.points: + raise HTTPException(status_code=422, detail='Awarded points exceed maximum points of the task') + else: + crud.approve_submission( + db, + submission_id=sub.id, + mentor_feedback=data.mentor_feedback, + points_awarded=data.points_awarded + ) + else: + crud.reject_submission( + db, + submission_id=sub.id, + mentor_feedback=data.mentor_feedback, + ) + return SubmissionOut(mentee_id=sub.mentee_id, + task_id=sub.task_id, + reference_link=sub.reference_link or None, + status=sub.status, + submitted_at=sub.submitted_at or None + ) + +@router.post("/pause-task", response_model=PauseTask) +def pause_task(data: PauseTask, db: Session = Depends(get_db)): + mentor = crud.get_user_by_email(db, email=data.mentor_email) + mentee = crud.get_user_by_email(db, email=data.mentee_email) + if not crud.is_mentor_of(db, mentor.id, mentee.id): + raise HTTPException(status_code=403, detail="Mentor not authorized for this mentee") + task = crud.get_task(db, task_no=data.task_no, track_id=data.track_id) + mentee_task_submission = crud.get_submission(db, mentee.email, task.track_id, task.task_no) + if mentee_task_submission.pause_start: + raise HTTPException(status_code=400, detail="This task is already paused") + crud.pause_task(db, mentee_task_submission.id) + return PauseTask(task_no=task.task_no, track_id=task.track_id, mentee_email=mentee.email, mentor_email=mentor.email) - # 4. Approve or pause - updated = crud.approve_submission( - db, - submission_id=sub.id, - mentor_feedback=data.mentor_feedback, - status=data.status - ) - return updated \ No newline at end of file +@router.post("/pause-end", response_model=PauseTask) +def end_pause(data: PauseTask, db: Session = Depends(get_db)): + mentor = crud.get_user_by_email(db, email=data.mentor_email) + mentee = crud.get_user_by_email(db, email=data.mentee_email) + if not crud.is_mentor_of(db, mentor.id, mentee.id): + raise HTTPException(status_code=403, detail="Mentor not authorized for this mentee") + task = crud.get_task(db, task_no=data.task_no, track_id=data.track_id) + mentee_task_submission = db.query(models.Submission).filter_by(mentee_id=mentee.id, task_id=task.id).first() + if not mentee_task_submission.pause_start: + raise HTTPException(status_code=400, detail="This task is not paused") + crud.end_pause(db, mentee_task_submission.id) + return PauseTask(task_no=task.task_no, track_id=task.track_id, mentee_email=mentee.email, mentor_email=mentor.email) + +@router.post("/start-task", response_model=StartTask) +def start_task(data: StartTask, db: Session = Depends(get_db)): + mentee = crud.get_user_by_email(db, email=data.mentee_email) + task = crud.get_task(db, track_id=data.track_id, task_no=data.task_no) + existing = crud.get_submission(db, mentee.email, task.track_id, task.task_no) + if existing: + raise HTTPException(status_code=403, detail="Already Started") + crud.start_task(db, task_id=task.id, mentee_id=mentee.id) + return mentee.email, task.task_no, task.track.id \ No newline at end of file diff --git a/app/routes/tracks.py b/app/routes/tracks.py index f200351..57ec4b0 100644 --- a/app/routes/tracks.py +++ b/app/routes/tracks.py @@ -2,8 +2,9 @@ from sqlalchemy.orm import Session from app.db import models from app.db.db import get_db -from app.schemas.track import TrackOut +from app.schemas.track import TrackOut, MenteeTasks from app.schemas.task import TaskOut +from app.db import crud router = APIRouter() @@ -12,9 +13,33 @@ def list_tracks(db: Session = Depends(get_db)): return db.query(models.Track).all() @router.get("/{track_id}/tasks", response_model=list[TaskOut]) -def list_tasks_for_track(track_id: int, db: Session = Depends(get_db)): - track = db.query(models.Track).filter_by(id=track_id).first() - if not track: - raise HTTPException(status_code=404, detail="Track not found") - - return db.query(models.Task).filter_by(track_id=track_id).order_by(models.Task.task_no).all() \ No newline at end of file +def mentee_specific_status(track_id: int, mentee_email: str, db: Session = Depends(get_db)): + tasks = db.query(models.Task).filter_by(track_id=track_id).order_by(models.Task.task_no).all() + if not tasks: + raise HTTPException(status_code=404, detail='Track not found') + mentee = crud.get_user_by_email(db, mentee_email) + if not mentee: + raise HTTPException(status_code=404, detail="Mentee not found") + tasks_with_status=[] + for task in tasks: + submission_of_task = db.query(models.Submission).filter_by(task_id=task.id, mentee_id=mentee.id).first() + if submission_of_task: + status = submission_of_task.status + time_spent = crud.find_time_spent_on_task(db, submission_of_task.id) + else: + status = "Not started" + time_spent = 0 + progress_bar = int((time_spent/task.deadline_days) * 100) + tasks_with_status.append( + { + "task_no": task.task_no, + "title": task.title, + "points": task.points, + "deadline": task.deadline_days, + "status": status, + "progress_bar": progress_bar, + "description": task.description, + "track_id": task.track_id + } + ) + return tasks_with_status \ No newline at end of file diff --git a/app/schemas/submission.py b/app/schemas/submission.py index b00954d..227d802 100644 --- a/app/schemas/submission.py +++ b/app/schemas/submission.py @@ -12,7 +12,7 @@ class SubmissionCreate(SubmissionBase): class SubmissionOut(BaseModel): - id: int + id: int mentee_id: int task_id: int reference_link: str @@ -27,5 +27,22 @@ class Config: class SubmissionApproval(BaseModel): submission_id: int mentor_email: str - status: str # approved, paused, rejected - mentor_feedback: Optional[str] = None \ No newline at end of file + status: str + mentor_feedback: Optional[str] = None + accepted: bool + points_awarded: int + +class PauseTask(BaseModel): + task_no: int + track_id: int + mentee_email: str + mentor_email: str + +class TasksList(BaseModel): + track_id: int + mentee_id: str + +class StartTask(BaseModel): + mentee_email: str + task_no: int + track_id: int \ No newline at end of file diff --git a/app/schemas/task.py b/app/schemas/task.py index 0a2de6b..4e0998d 100644 --- a/app/schemas/task.py +++ b/app/schemas/task.py @@ -8,13 +8,14 @@ class TaskBase(BaseModel): title: str description: Optional[str] = None points: int = 10 - deadline: Optional[datetime] = None + deadline: Optional[int] = None class TaskCreate(TaskBase): pass class TaskOut(TaskBase): - id: int + progress_bar: int + status: str class Config: orm_mode = True \ No newline at end of file diff --git a/app/schemas/track.py b/app/schemas/track.py index 0ac8fdd..e6b982c 100644 --- a/app/schemas/track.py +++ b/app/schemas/track.py @@ -12,4 +12,19 @@ class TrackOut(TrackBase): id: int class Config: - orm_mode = True \ No newline at end of file + orm_mode = True + +class TaskOut(BaseModel): + task_no: int + title: str + points: int + deadline: int + status: str + time_spent: int + description: str + track: str + +class MenteeTasks(BaseModel): + mentee_email: str + task_no: int + track_id: int \ No newline at end of file