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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Implemented Features Include:
- Add/remove/modify extensions for an assignment in a course
- Add/remove/modify dates for an assignment in a course
- Upload submissions to assignments
- Create new assignments
- API server to interact with library without Python

## Demo
Expand Down
91 changes: 90 additions & 1 deletion src/gradescopeapi/_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,37 @@
Configuration file for FastAPI. Specifies the specific objects and data models used in our api
"""

import enum
import io
from datetime import datetime

from pydantic import BaseModel
from pydantic import BaseModel, field_validator, model_validator


class SubmissionType(enum.Enum):
"""Gradescope submission type.

VARIABLE corresponds to image-based submissions (student uploads images).
FIXED corresponds to PDF template submissions (fixed-format PDF).
"""

VARIABLE = "image"
FIXED = "pdf"


class WhenToCreateRubric(enum.Enum):
"""When the rubric should be made available on Gradescope."""

WHILE_GRADING = "while_grading"
BEFORE_SUBMISSIONS = "before_submissions"


class RubricLockingSetting(enum.Enum):
"""Who can edit the rubric on Gradescope."""

ALL_EDIT = "all_edit"
INSTRUCTOR_EDIT = "instructor_edit"
NO_EDIT = "no_edit"


class UserSession(BaseModel):
Expand Down Expand Up @@ -63,3 +90,65 @@ class AssignmentUpload(BaseModel):
course_id: str
assignment_id: str
leaderboard_name: str | None = None


class StudentSubmissionSettings(BaseModel):
release_date: datetime
due_date: datetime
allow_late_submissions: bool
late_due_date: datetime | None = None
time_limit_in_minutes: int | None = None
submission_type: SubmissionType
group_submission: bool = False
group_size: int | None = None
template_visible_to_students: bool = False

@model_validator(mode="after")
def _validate_dates_and_tz(self):
if self.due_date < self.release_date:
raise ValueError("due_date must be after release_date")
if self.late_due_date is not None:
if not self.allow_late_submissions:
raise ValueError(
"late_due_date can only be set when allow_late_submissions is True"
)
if self.late_due_date <= self.due_date:
raise ValueError("late_due_date must be after due_date")
return self

@field_validator("group_size")
@classmethod
def _validate_group_size(cls, v):
if v is not None and v < 2:
raise ValueError("group_size must be at least 2")
return v

@field_validator("time_limit_in_minutes")
@classmethod
def _validate_time_limit(cls, v):
if v is not None and v <= 0:
raise ValueError("time_limit_in_minutes must be positive")
return v


class CreateAssignment(BaseModel):
course_id: str
title: str
submissions_anonymized: bool = False
student_submission: bool = False
student_submission_settings: StudentSubmissionSettings | None = None
when_to_create_rubric: WhenToCreateRubric = WhenToCreateRubric.WHILE_GRADING
rubric_locking_setting: RubricLockingSetting = RubricLockingSetting.ALL_EDIT

@model_validator(mode="after")
def _validate_student_submission_settings(self):
if self.student_submission_settings is not None and not self.student_submission:
raise ValueError(
"student_submission_settings can only be set when student_submission is True"
)
if self.student_submission and self.student_submission_settings is None:
raise ValueError(
"student_submission is True but no config is passed in "
"(student_submission_settings is required)"
)
return self
117 changes: 113 additions & 4 deletions src/gradescopeapi/api/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
from datetime import datetime

from fastapi import Depends, FastAPI, HTTPException, status

from gradescopeapi._config.config import FileUploadModel, LoginRequestModel
import requests
from fastapi import Depends, FastAPI, Form, HTTPException, UploadFile, status

from gradescopeapi._config.config import (
CreateAssignment,
FileUploadModel,
LoginRequestModel,
RubricLockingSetting,
StudentSubmissionSettings,
WhenToCreateRubric,
)
from gradescopeapi.classes.account import Account
from gradescopeapi.classes.assignments import Assignment, update_assignment_date
from gradescopeapi.classes.assignments import (
Assignment,
AssignmentUpdateError,
create_assignment,
update_assignment_date,
)
from gradescopeapi.classes.connection import GSConnection
from gradescopeapi.classes.courses import Course
from gradescopeapi.classes.extensions import get_extensions, update_student_extension
Expand Down Expand Up @@ -380,3 +393,99 @@ def upload_assignment_files(
raise HTTPException(status_code=400, detail="Upload unsuccessful")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@app.post("/assignments/create")
def create_new_assignment(
course_id: str = Form(...),
title: str = Form(...),
submissions_anonymized: bool = Form(False),
student_submission: bool = Form(False),
student_submission_settings: str | None = Form(None),
when_to_create_rubric: WhenToCreateRubric = Form(WhenToCreateRubric.WHILE_GRADING),
rubric_locking_setting: RubricLockingSetting = Form(RubricLockingSetting.ALL_EDIT),
template_pdf: UploadFile | None = None,
):
"""
Create a new assignment in a course. The session must have instructor privileges.

The student_submission_settings field is a JSON string describing the
StudentSubmissionSettings model (release_date, due_date, submission_type, etc.).

Args:
course_id (str): The ID of the course.
title (str): The title of the new assignment.
submissions_anonymized (bool, optional): Anonymize submissions. Defaults to False.
student_submission (bool, optional): Whether students submit. Defaults to False.
student_submission_settings (str | None, optional): JSON settings. Defaults to None.
when_to_create_rubric (WhenToCreateRubric, optional): Rubric timing. Defaults to WHILE_GRADING.
rubric_locking_setting (RubricLockingSetting, optional): Rubric locking. Defaults to ALL_EDIT.
template_pdf (UploadFile | None, optional): Template PDF file. Defaults to None.

Returns:
dict: A dictionary containing the new assignment ID.

Raises:
HTTPException: 400 if validation or creation fails, 500 on unexpected errors.
"""
settings = None
if student_submission_settings:
settings = StudentSubmissionSettings.model_validate_json(
student_submission_settings
)

try:
create_data = CreateAssignment(
course_id=course_id,
title=title,
submissions_anonymized=submissions_anonymized,
student_submission=student_submission,
student_submission_settings=settings,
when_to_create_rubric=when_to_create_rubric,
rubric_locking_setting=rubric_locking_setting,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

pdf_file = template_pdf.file if template_pdf else None
pdf_filename = template_pdf.filename if template_pdf else None
if pdf_file is not None:
pdf_file.seek(0, 2) # seek to end to get file size
file_size = pdf_file.tell()
pdf_file.seek(0) # rewind to start
if file_size > 50 * 1024 * 1024: # 50 MB limit
raise HTTPException(
status_code=413,
detail=f"Template PDF size {file_size} bytes exceeds 50 MB limit",
)

try:
assignment_id = create_assignment(
session=connection.session,
course_id=create_data.course_id,
title=create_data.title,
template_pdf=pdf_file,
template_pdf_filename=pdf_filename,
submissions_anonymized=create_data.submissions_anonymized,
student_submission=create_data.student_submission,
student_submission_settings=create_data.student_submission_settings,
when_to_create_rubric=create_data.when_to_create_rubric,
rubric_locking_setting=create_data.rubric_locking_setting,
)
return {
"assignment_id": assignment_id,
"status_code": status.HTTP_200_OK,
}
except AssignmentUpdateError as e:
raise HTTPException(status_code=400, detail=str(e))
except requests.HTTPError as e:
raise HTTPException(
status_code=e.response.status_code
if e.response is not None
else status.HTTP_502_BAD_GATEWAY,
detail=str(e),
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Loading