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
220 changes: 196 additions & 24 deletions cs17_portal/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import frappe
from frappe import _
from frappe.utils import now_datetime

if TYPE_CHECKING:
from frappe.model.document import Document
Expand All @@ -17,23 +18,6 @@ def get_user_profile() -> dict | None:
)


@frappe.whitelist(methods=["GET"])
def get_recent_submissions(faculty: str, limit: int = 5) -> list:
profile = frappe.get_doc("CS17 Profile", faculty)
if profile.user != frappe.session.user:
frappe.throw(_("Not permitted"), frappe.PermissionError)
if not profile.cohort:
return []
return frappe.get_list(
"CS17 Assignment Submission",
filters=[["assignment.cohort", "=", profile.cohort]],
fields=["name", "student", "full_name", "assignment", "assignment_title", "submitted_at"],
order_by="submitted_at desc",
limit=limit,
ignore_permissions=True,
)


@frappe.whitelist(methods=["GET"])
def get_faculty_assignments(cohort: str | None = None) -> list:
validate_membership("Faculty")
Expand Down Expand Up @@ -72,6 +56,41 @@ def _attach_submission_counts(assignments: list) -> None:
assignment["submission_count"] = counts.get(assignment["name"], 0)


@frappe.whitelist(methods=["GET"])
def get_student_assignments(cohort: str) -> dict:
"""Assignments a student can see now, plus the next scheduled publish time.

Visibility is gated on server time, not the scheduler flag: an assignment shows the
instant `publish_on` passes, so a scheduled publish is exact to the second regardless of
when the background job flips `is_published`. `next_publish_on` lets the client reveal the
next one at the precise moment without polling.
"""
now = now_datetime()
assignments = frappe.get_all(
"CS17 Assignment",
filters={"cohort": cohort},
or_filters=[["is_published", "=", 1], ["publish_on", "<=", now]],
fields=[
"name",
"title",
"due_date",
"max_marks",
"assignment_type",
"submission_type",
"modified",
],
order_by="due_date desc",
)
upcoming = frappe.get_all(
"CS17 Assignment",
filters=[["cohort", "=", cohort], ["is_published", "=", 0], ["publish_on", ">", now]],
fields=["publish_on"],
order_by="publish_on asc",
limit=1,
)
return {"assignments": assignments, "next_publish_on": upcoming[0].publish_on if upcoming else None}


@frappe.whitelist(methods=["POST"])
def create_assignment(
title: str,
Expand All @@ -87,23 +106,102 @@ def create_assignment(
) -> str:
validate_membership("Faculty")
assignment = frappe.new_doc("CS17 Assignment")
assignment.update(
assignment.naming_series = _naming_series(assignment_type)
_set_assignment_fields(
assignment, title, cohort, due_date, submission_type, description, assignment_type, max_marks, remarks
)
_apply_publish_state(assignment, publish, publish_on)
assignment.insert(ignore_permissions=True)
return assignment.name


@frappe.whitelist(methods=["POST"])
def update_assignment(
assignment: str,
title: str,
cohort: str,
due_date: str,
submission_type: str = "Any",
description: str | None = None,
assignment_type: str = "Not Graded",
max_marks: float = 0,
remarks: str = "Grade",
publish: str = "draft",
publish_on: str | None = None,
) -> str:
validate_membership("Faculty")
doc = frappe.get_doc("CS17 Assignment", assignment)
_set_assignment_fields(
doc, title, cohort, due_date, submission_type, description, assignment_type, max_marks, remarks
)
_apply_publish_state(doc, publish, publish_on)
doc.save(ignore_permissions=True)
return doc.name


def _set_assignment_fields(
doc: "Document",
title: str,
cohort: str,
due_date: str,
submission_type: str,
description: str | None,
assignment_type: str,
max_marks: float,
remarks: str,
) -> None:
doc.update(
{
"title": title,
"cohort": cohort,
"due_date": due_date,
"submission_type": submission_type,
"description": description,
"assignment_type": assignment_type,
"naming_series": _naming_series(assignment_type),
}
)
if assignment_type == "Graded":
assignment.max_marks = max_marks
assignment.remarks = remarks
_apply_publish_state(assignment, publish, publish_on)
assignment.insert(ignore_permissions=True)
return assignment.name
doc.max_marks = max_marks
doc.remarks = remarks


@frappe.whitelist(methods=["GET"])
def get_assignment(assignment: str) -> dict | None:
validate_membership("Faculty")
return frappe.db.get_value(
"CS17 Assignment",
assignment,
[
"name",
"title",
"description",
"due_date",
"cohort",
"submission_type",
"assignment_type",
"max_marks",
"remarks",
"is_published",
"publish_on",
],
as_dict=True,
)


@frappe.whitelist(methods=["POST"])
def delete_assignment(assignment: str) -> None:
validate_membership("Faculty")
if frappe.db.exists("CS17 Assignment Submission", {"assignment": assignment}):
frappe.throw(_("Cannot delete an assignment that already has submissions"))
frappe.delete_doc("CS17 Assignment", assignment, ignore_permissions=True)


@frappe.whitelist(methods=["POST"])
def publish_assignment(assignment: str, publish: str = "now", publish_on: str | None = None) -> None:
validate_membership("Faculty")
doc = frappe.get_doc("CS17 Assignment", assignment)
_apply_publish_state(doc, publish, publish_on)
doc.save(ignore_permissions=True)


def _naming_series(assignment_type: str) -> str:
Expand Down Expand Up @@ -157,6 +255,7 @@ def get_assignment_submissions(assignment: str) -> dict:
"submitted_at",
"submission_document",
"submission_url",
"_assign",
],
order_by="submitted_at desc",
)
Expand Down Expand Up @@ -214,6 +313,79 @@ def grade_submission(
return {"name": doc.name}


ASSIGNMENT_SUBMISSION = "CS17 Assignment Submission"


@frappe.whitelist(methods=["GET"])
def get_assigned_submissions(limit: int = 10) -> list:
validate_membership("Faculty")
names = _assigned_submission_names(frappe.session.user, limit)
if not names:
return []
submissions = frappe.get_all(
ASSIGNMENT_SUBMISSION,
filters=[["name", "in", names]],
fields=["name", "student", "full_name", "assignment", "assignment_title", "submitted_at"],
ignore_permissions=True,
)
_attach_grades(submissions)
return _order_by_names(submissions, names)


def _assigned_submission_names(user: str, limit: int) -> list:
todos = frappe.get_all(
"ToDo",
filters={"reference_type": ASSIGNMENT_SUBMISSION, "allocated_to": user, "status": "Open"},
fields=["reference_name"],
order_by="creation desc",
limit=limit,
)
return [todo["reference_name"] for todo in todos]


def _order_by_names(rows: list, ordered_names: list) -> list:
by_name = {row["name"]: row for row in rows}
return [by_name[name] for name in ordered_names if name in by_name]


@frappe.whitelist(methods=["POST"])
def assign_submission(submission: str, assign_to: str) -> None:
validate_membership("Faculty")
_assign_submission_to(submission, assign_to)


@frappe.whitelist(methods=["POST"])
def assign_submissions(submissions: list | str, assign_to: str) -> None:
validate_membership("Faculty")
for submission in frappe.parse_json(submissions):
_assign_submission_to(submission, assign_to)


def _assign_submission_to(submission: str, assign_to: str) -> None:
from frappe.desk.form.assign_to import add

add({"doctype": ASSIGNMENT_SUBMISSION, "name": submission, "assign_to": [assign_to]})


@frappe.whitelist(methods=["POST"])
def unassign_submission(submission: str, assign_to: str) -> None:
validate_membership("Faculty")
from frappe.desk.form.assign_to import remove

remove(ASSIGNMENT_SUBMISSION, submission, assign_to)


@frappe.whitelist(methods=["GET"])
def get_faculty_members() -> list:
validate_membership("Faculty")
return frappe.get_all(
"CS17 Profile",
filters={"profile_type": "Faculty", "user": ["is", "set"]},
fields=["user", "full_name"],
order_by="full_name asc",
)


def get_current_profile_name(profile_type: str) -> str | None:
return frappe.db.get_value(
"CS17 Profile",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,14 @@
"fieldname": "due_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Due Date",
"reqd": 1
"label": "Due Date"
},
{
"fieldname": "cohort",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cohort",
"options": "CS17 Cohort",
"reqd": 1
"options": "CS17 Cohort"
},
{
"default": "Any",
Expand Down
10 changes: 10 additions & 0 deletions cs17_portal/cs17_portal/doctype/cs17_assignment/cs17_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# For license information, please see license.txt

import frappe
from frappe import _
from frappe.model.document import Document


Expand Down Expand Up @@ -36,3 +37,12 @@ def validate(self):
if self.assignment_type == "Not Graded":
self.max_marks = 0
self.remarks = ""
self._validate_publishable()

def _validate_publishable(self):
if not (self.is_published or self.publish_on):
return
if not self.cohort:
frappe.throw(_("A cohort is required to publish an assignment"))
if not self.due_date:
frappe.throw(_("A due date is required to publish an assignment"))
4 changes: 2 additions & 2 deletions dashboard/src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function SelectContent({
color: "var(--popover-foreground)",
}}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-32 origin-[--radix-select-content-transform-origin] overflow-hidden rounded-lg border border-border shadow-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-32 origin-[var(--radix-select-content-transform-origin)] overflow-hidden rounded-lg border border-border shadow-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
className
Expand All @@ -66,7 +66,7 @@ function SelectContent({
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
"max-h-[var(--radix-select-content-available-height)] overflow-y-auto p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
Expand Down
Loading
Loading