Skip to content

Commit

Permalink
Add lots of docstrings (#52)
Browse files Browse the repository at this point in the history
Models
- [X] `assignments/models.py`
- [x] `courses/models.py`
- [x] `venvs/models.py`
- [x] `submissions/models.py`
- [x] `users/models.py`

Views
- [x] `assignments/views.py`
- [x] `courses/views.py`
- [X] `submissions/views.py`
- [x] `venvs/views.py`
- [x] `errors/views.py`
- [x] `auth/views.py`
- [x] `docs/views.py`
  • Loading branch information
JasonGrace2282 committed Aug 3, 2024
1 parent 82fdc10 commit c15ae7b
Show file tree
Hide file tree
Showing 13 changed files with 554 additions and 45 deletions.
88 changes: 79 additions & 9 deletions tin/apps/assignments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@


class Folder(models.Model):
"""A folder for assignments.
Each course can have multiple folders, and each
assignment can be in a folder.
"""

name = models.CharField(max_length=50)

course = models.ForeignKey("courses.Course", on_delete=models.CASCADE, related_name="folders")
Expand All @@ -39,6 +45,17 @@ def __repr__(self):

class AssignmentQuerySet(models.query.QuerySet):
def filter_permissions(self, user, *perms: Literal["-", "r", "w"]):
"""Filters based off of the permissions of the user and the course.
An admin can always see everything. A teacher can only see courses they
are the teachers for. Otherwise it filters it based off the course being archived
and/or the permission of the course after archiving.
Args:
user: The user executing the query (``request.user``)
*perms: Every permission listed is or-ed together.
Each value can be - (hidden), r (read-only), or w (read-write)
"""
if user.is_superuser:
return self.all()
else:
Expand All @@ -50,19 +67,33 @@ def filter_permissions(self, user, *perms: Literal["-", "r", "w"]):
return self.filter(q).distinct()

def filter_visible(self, user):
r"""Filters assignments that are visible to a user
Alias for calling :meth:`filter_permissions` with the permissions
"r" and "w"
"""
return self.filter_permissions(user, "r", "w")

def filter_submittable(self, user):
"""Filters by assignments that can be submitted.
.. warning::
Do NOT use this if :attr:`~Assignment.is_quiz` is ``True``.
In that case, the check should be done manually.
"""
return self.filter_permissions(user, "w")

def filter_editable(self, user):
"""Filters assignments if they're editable by the user"""
if user.is_superuser:
return self.all()
else:
return self.filter(course__teacher=user).distinct()


def upload_grader_file_path(assignment, _): # pylint: disable=unused-argument
"""Get the location of the grader file for an assignment"""
assert assignment.id is not None
if assignment.language == "P":
return f"assignment-{assignment.id}/grader.py"
Expand All @@ -71,6 +102,14 @@ def upload_grader_file_path(assignment, _): # pylint: disable=unused-argument


class Assignment(models.Model):
"""An assignment (or quiz) for a student.
If :attr:`~.Assignment.is_quiz` is ``True``, this
model doubles as a quiz.
The manager for this model is :class:`.AssignmentQuerySet`.
"""

name = models.CharField(max_length=50)
folder = models.ForeignKey(
Folder,
Expand Down Expand Up @@ -154,17 +193,22 @@ def __repr__(self):
return self.name

def make_assignment_dir(self) -> None:
"""Creates the directory where the assignment grader scripts go."""
assignment_path = os.path.join(settings.MEDIA_ROOT, f"assignment-{self.id}")
os.makedirs(assignment_path, exist_ok=True)

def save_grader_file(self, grader_text: str) -> None:
# Writing to files in directories not controlled by us without some
# form of sandboxing is a security risk. Most notably, users can use symbolic
# links to trick you into writing to another file, outside the directory.
# they control.
# This solution is very hacky, but we don't have another good way of
# doing this.
"""Save the grader file to the correct location.
.. warning::
Writing to files in directories not controlled by us without some
form of sandboxing is a security risk. Most notably, users can use symbolic
links to trick you into writing to another file, outside the directory.
they control.
This solution is very hacky, but we don't have another good way of
doing this.
"""
fname = upload_grader_file_path(self, "")

self.grader_file.name = fname
Expand Down Expand Up @@ -195,6 +239,15 @@ def save_grader_file(self, grader_text: str) -> None:
raise FileNotFoundError from e

def list_files(self) -> list[tuple[int, str, str, int, datetime.datetime]]:
"""List all files in the assignments directory
Returns:
- The index of the assignment
- The name of the assignment submission
- The full path to the assignment submission
- The size of the submission
- The time at which it was submitted
"""
self.make_assignment_dir()

assignment_path = os.path.join(settings.MEDIA_ROOT, f"assignment-{self.id}")
Expand All @@ -219,6 +272,7 @@ def list_files(self) -> list[tuple[int, str, str, int, datetime.datetime]]:
return files

def save_file(self, file_text: str, file_name: str) -> None:
"""Save some text as a file"""
self.make_assignment_dir()

fpath = os.path.join(settings.MEDIA_ROOT, f"assignment-{self.id}", file_name)
Expand Down Expand Up @@ -256,6 +310,7 @@ def get_file(self, file_id: int) -> tuple[str, str]:
return "", ""

def delete_file(self, file_id: int) -> None:
"""Delete a file by id"""
self.make_assignment_dir()

for i, item in enumerate(
Expand All @@ -266,6 +321,7 @@ def delete_file(self, file_id: int) -> None:
return

def check_rate_limit(self, student) -> None:
"""Check if a student is submitting too quickly"""
now = timezone.localtime()

if (
Expand Down Expand Up @@ -293,18 +349,22 @@ def grader_log_filename(self) -> str:
return f"{upload_grader_file_path(self, '').rsplit('.', 1)[0]}.log"

def quiz_open_for_student(self, student):
"""Check if a quiz is open for a specific student"""
is_teacher = self.course.teacher.filter(id=student.id).exists()
if is_teacher or student.is_superuser:
return True
return not (self.quiz_ended_for_student(student) or self.quiz_locked_for_student(student))

def quiz_ended_for_student(self, student):
def quiz_ended_for_student(self, student) -> bool:
"""Check if the quiz has ended for a student"""
return self.log_messages.filter(student=student, content="Ended quiz").exists()

def quiz_locked_for_student(self, student):
def quiz_locked_for_student(self, student) -> bool:
"""Check if the quiz has been locked (e.g. due to leaving the tab)"""
return self.quiz_issues_for_student(student) and self.quiz_action == "2"

def quiz_issues_for_student(self, student):
def quiz_issues_for_student(self, student) -> bool:
"""Check if the student has exceeded the maximum amount of issues they can have with a quiz."""
return (
sum(lm.severity for lm in self.log_messages.filter(student=student))
>= settings.QUIZ_ISSUE_THRESHOLD
Expand Down Expand Up @@ -411,6 +471,8 @@ def ended_for_student(self, student):


class QuizLogMessage(models.Model):
"""A log message for an :class:`Assignment` (with :attr:`~.Assignment.is_quiz` set to ``True``)"""

assignment = models.ForeignKey(
Assignment, on_delete=models.CASCADE, related_name="log_messages"
)
Expand Down Expand Up @@ -504,6 +566,11 @@ def __repr__(self):


def run_action(command: list[str]) -> str:
"""Runs a command.
If the command cannot find a file, raises an exception.
Otherwise, returns the stdout of the command.
"""
try:
res = subprocess.run(
command,
Expand All @@ -521,6 +588,8 @@ def run_action(command: list[str]) -> str:


class FileAction(models.Model):
"""Runs a user uploaded script on files uploaded to an assignment."""

MATCH_TYPES = (("S", "Start with"), ("E", "End with"), ("C", "Contain"))

name = models.CharField(max_length=50)
Expand All @@ -541,6 +610,7 @@ def __repr__(self):
return self.name

def run(self, assignment: Assignment):
"""Runs the command on the input assignment"""
command = self.command.split(" ")

if (
Expand Down
Loading

0 comments on commit c15ae7b

Please sign in to comment.