-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #62 from CSCI128/feat_gjbell_bartik_support
Add support to grade bartik
- Loading branch information
Showing
23 changed files
with
495 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from typing import List | ||
from azure.identity import AzureCliCredential | ||
from azure.core.credentials import TokenCredential | ||
from msgraph import GraphServiceClient | ||
from msgraph.generated.users.users_request_builder import UsersRequestBuilder | ||
|
||
|
||
class AzureAD(): | ||
|
||
SCOPES: List[str] = ["https://graph.microsoft.com/.default"] | ||
|
||
@staticmethod | ||
def _authenticate(tenantId: str) -> TokenCredential: | ||
# TODO - We will need to provide hints for this | ||
cred = AzureCliCredential(tenant_id=tenantId) | ||
|
||
return cred | ||
|
||
|
||
@staticmethod | ||
def _createGraphServiceClient(cred: TokenCredential, scopes = SCOPES) -> GraphServiceClient: | ||
client = GraphServiceClient(cred, scopes) | ||
|
||
return client | ||
|
||
|
||
|
||
def __init__(self, tenantId: str) -> None: | ||
self.cred = self._authenticate(tenantId) | ||
self.client = self._createGraphServiceClient(self.cred) | ||
|
||
|
||
|
||
async def getCWIDFromEmail(self, username: str) -> str: | ||
query = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( | ||
select=["employeeId"], | ||
) | ||
|
||
requestConfig = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(query_parameters=query) | ||
userCwid = await self.client.users.by_user_id(username).get(requestConfig) | ||
|
||
if userCwid is None or userCwid.employee_id is None: | ||
return "" | ||
|
||
return userCwid.employee_id | ||
|
||
async def getEmailFromCWID(self, cwid: str) -> str: | ||
query = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( | ||
select=["userPrincipalName"], | ||
filter=f"employeeId eq '{cwid}' and accountEnabled eq true", | ||
) | ||
|
||
requestConfig = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(query_parameters=query) | ||
|
||
user = await self.client.users.get(requestConfig) | ||
|
||
if user is None: | ||
return "" | ||
|
||
if user.value is None or not len(user.value): | ||
return "" | ||
|
||
user = user.value[0] | ||
|
||
if user.user_principal_name is None: | ||
return "" | ||
|
||
return user.user_principal_name | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from datetime import datetime | ||
|
||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
class Assignments(Base): | ||
__tablename__ = "assignments" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
# Technically this is nullable, but there are no instances in the DB where it is null | ||
name: Mapped[str] | ||
created_at: Mapped[datetime] | ||
updated_at: Mapped[datetime] | ||
due_date: Mapped[datetime] | ||
hidden: Mapped[bool] = mapped_column(default=False) | ||
description: Mapped[str] | ||
course_id: Mapped[int] = mapped_column(index=True) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
class AssignmentsProblemsMap(Base): | ||
__tablename__ = "assignments_problems" | ||
|
||
# Composite keys have to both be marked as the primary | ||
# ffs this took like 2 hours of debugging | ||
assignment_id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
problem_id: Mapped[int] = mapped_column(primary_key=True, index=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
from typing import List, Optional | ||
from sqlalchemy import create_engine, Engine, select | ||
from sqlalchemy.orm import Session, sessionmaker | ||
from Bartik.Assignments import Assignments | ||
from Bartik.AssignmentsProblemsMap import AssignmentsProblemsMap | ||
from Bartik.Courses import Courses | ||
from Bartik.Users import Users | ||
from Bartik.Grades import Grades | ||
from Bartik.Base import Base | ||
|
||
|
||
class Bartik(): | ||
|
||
def __init__(self, _url: str, _userName: str, _password: str, courseId: Optional[int] = None) -> None: | ||
CONNECTION_STRING: str = f"postgresql+psycopg://{_userName}:{_password}@{_url}/autograder" | ||
|
||
self.engine: Engine = create_engine(CONNECTION_STRING) | ||
|
||
self.BoundSession = sessionmaker(bind=self.engine) | ||
|
||
self.session: Optional[Session] = None | ||
|
||
self.COURSE_ID = courseId | ||
|
||
Base.metadata.create_all(self.engine) | ||
|
||
|
||
def openSession(self): | ||
if self.session is not None: | ||
return | ||
|
||
self.session = self.BoundSession() | ||
|
||
def closeSession(self): | ||
if self.session is None: | ||
return | ||
|
||
self.session.close() | ||
|
||
def getCourseId(self, _courseName: str) -> int: | ||
if self.session is None: | ||
raise Exception("Session must be started") | ||
|
||
courseIdStm = select(Courses).where(Courses.name.like(f"%{_courseName}%"), Courses.active==True) | ||
courseIdCourse = self.session.scalars(courseIdStm).first() | ||
|
||
if courseIdCourse is None: | ||
raise Exception("Failed to locate course") | ||
|
||
return courseIdCourse.id | ||
|
||
|
||
|
||
def getScoreForAssignment(self, _email: str, _assessment: str, requiredProblems: int = 3, maxScore: float = 10) -> float: | ||
if self.session is None: | ||
raise Exception("Session must be started") | ||
|
||
assessmentIdStm = select(Assignments).where(Assignments.name.like(f"%{_assessment}%"), Assignments.course_id == self.COURSE_ID) | ||
assessmentIdAssessment = self.session.scalars(assessmentIdStm).first() | ||
|
||
if assessmentIdAssessment is None: | ||
raise Exception("Failed to locate assignment") | ||
|
||
assessmentId: int = assessmentIdAssessment.id | ||
|
||
problemsIdStm = select(AssignmentsProblemsMap).where(AssignmentsProblemsMap.assignment_id == assessmentId) | ||
|
||
problemsIdProblems = self.session.scalars(problemsIdStm).all() | ||
|
||
if problemsIdProblems is None or not len(problemsIdProblems): | ||
raise Exception("Failed to locate problems for assignment") | ||
|
||
problemIds: List[int] = [problemId.problem_id for problemId in problemsIdProblems if problemId is not None] | ||
|
||
userIdStm = select(Users).where(Users.email == _email) | ||
userIdUser = self.session.scalars(userIdStm).first() | ||
|
||
if userIdUser is None: | ||
raise Exception(f"Failed to find user with email {_email}") | ||
|
||
userId = userIdUser.id | ||
|
||
gradesStm = select(Grades).where(Grades.problem_id.in_(problemIds), Grades.user_id == userId) | ||
grades = self.session.scalars(gradesStm).all() | ||
# Rn i dont really have the motivation to parse the required problems | ||
|
||
totalScore: float = 0 | ||
|
||
for grade in grades: | ||
totalScore += grade.score if grade is not None else 0 | ||
|
||
# bartik reports scores out of 10 per problem, scale down so they are now out of 1 | ||
|
||
totalScore /= 10 | ||
|
||
totalScore = round((totalScore / requiredProblems) * maxScore, 2) | ||
|
||
return totalScore if totalScore <= maxScore else maxScore | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from sqlalchemy.orm import DeclarativeBase | ||
|
||
|
||
class Base(DeclarativeBase): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from datetime import datetime | ||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
class Courses(Base): | ||
__tablename__ = "courses" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
course_id: Mapped[str] | ||
name: Mapped[str] | ||
term: Mapped[str] | ||
active: Mapped[bool] | ||
created_at: Mapped[datetime] | ||
updated_at: Mapped[datetime] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from datetime import datetime | ||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
class Grades(Base): | ||
__tablename__ = "grades" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
score: Mapped[int] | ||
user_id: Mapped[int] = mapped_column(index=True) | ||
problem_id: Mapped[int] = mapped_column(index=True) | ||
created_at: Mapped[datetime] | ||
updated_at: Mapped[datetime] | ||
assignment_id: Mapped[int] | ||
course_id: Mapped[int] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from datetime import datetime | ||
from typing import Optional | ||
|
||
from sqlalchemy import BigInteger, DateTime | ||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
class Users(Base): | ||
__tablename__ = "users" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
description: Mapped[str] | ||
name: Mapped[str] | ||
created_at: Mapped[datetime] | ||
updated_at: Mapped[datetime] | ||
hover_description: Mapped[str] | ||
tests_json: Mapped[str] | ||
spec: Mapped[str] | ||
hidden_tests: Mapped[str] | ||
function_name: Mapped[str] | ||
in_type: Mapped[str] | ||
out_type: Mapped[str] | ||
programming_language: Mapped[str] | ||
header: Mapped[str] | ||
input_conversion_functions: Mapped[str] | ||
output_conversion_function: Mapped[str] | ||
text_files: Mapped[str] | ||
input_or_output: Mapped[str] | ||
text_filename: Mapped[str] | ||
input_method: Mapped[str] | ||
output_method: Mapped[str] | ||
input_filename: Mapped[str] | ||
output_filename: Mapped[str] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from datetime import datetime | ||
from typing import Optional | ||
from Bartik.Base import Base | ||
from sqlalchemy.orm import Mapped | ||
from sqlalchemy.orm import mapped_column | ||
|
||
|
||
class Users(Base): | ||
__tablename__ = "users" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, index=True) | ||
multipass_id: Mapped[str] | ||
created_at: Mapped[datetime] | ||
updated_at: Mapped[datetime] | ||
email: Mapped[str] = mapped_column(default="", unique=True, index=True) | ||
provider: Mapped[str] | ||
uid: Mapped[str] | ||
name: Mapped[str] | ||
role: Mapped[str] | ||
theme: Mapped[str] = mapped_column(default="chrome") | ||
keybind: Mapped[str] = mapped_column(default="ace") | ||
current_course_id: Mapped[int] | ||
successColor: Mapped[str] = mapped_column(default="#5cb85c") | ||
dangerColor: Mapped[str] = mapped_column(default="#d9534f") | ||
infoColor: Mapped[str] = mapped_column(default="#5bc0de") | ||
warningColor: Mapped[str] = mapped_column(default="#f0ad4e") | ||
incorrectBar: Mapped[bool] = mapped_column(default=False) | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.