Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Deadlines in Assignment List #1909

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions nbgrader/coursedir.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ def _db_url_default(self):
)
).tag(config=True)

deadline_file = Unicode(
"deadlines.json",
help="File for storing deadlines"
).tag(config=True)

groupshared = Bool(
False,
help=dedent(
Expand Down
3 changes: 3 additions & 0 deletions nbgrader/server_extensions/assignment_list/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ...coursedir import CourseDirectory
from ...auth import Authenticator
from ... import __version__ as nbgrader_version
from ..helpers.deadline import DeadlineManager


static = os.path.join(os.path.dirname(__file__), 'static')
Expand Down Expand Up @@ -83,6 +84,8 @@ def list_released_assignments(self, course_id=None):
authenticator=authenticator,
config=config)
assignments = lister.start()
assignments = DeadlineManager(config.Exchange.root, coursedir, self.log) \
.fetch_deadlines(assignments)

except Exception as e:
self.log.error(traceback.format_exc())
Expand Down
3 changes: 3 additions & 0 deletions nbgrader/server_extensions/formgrader/apihandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from tornado import web

from ...server_extensions.helpers.deadline import DeadlineManager
from .base import BaseApiHandler, check_xsrf, check_notebook_dir
from ...api import MissingEntry

Expand Down Expand Up @@ -143,6 +144,8 @@ def put(self, assignment_id):
assignment = {"duedate": duedate}
assignment_id = assignment_id.strip()
self.gradebook.update_or_create_assignment(assignment_id, **assignment)
DeadlineManager(self.api.exchange_root, self.coursedir, self.log) \
.update_or_add_deadline(assignment_id, duedate)
sourcedir = os.path.abspath(self.coursedir.format_path(self.coursedir.source_directory, '.', assignment_id))
if not os.path.isdir(sourcedir):
os.makedirs(sourcedir)
Expand Down
90 changes: 90 additions & 0 deletions nbgrader/server_extensions/helpers/deadline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import json
import os
from zoneinfo import ZoneInfo

from ... import utils


class DeadlineManager:
def __init__(self, exchange_root, coursedir, logger):
self.exchange_root = exchange_root
self.coursedir = coursedir
self.log = logger

def fetch_deadlines(self, assignments):
"""Fetch the deadline for the given course id and assignments."""
if not self.exchange_root: # Non-FS based exchange
return assignments

dir_name = os.path.join(self.exchange_root, self.coursedir.course_id)
file_path = os.path.join(dir_name, self.coursedir.deadline_file)
if not os.path.exists(file_path) or not os.path.isfile(file_path):
self.log.warning("No deadlines file found at {}".format(file_path))
return assignments

deadlines = self._read_deadlines(file_path)
for assignment in assignments:
if assignment["assignment_id"] in deadlines:
assignment["due_date"] = deadlines[assignment["assignment_id"]]
return assignments

def update_or_add_deadline(self, assignment_id, deadline):
"""Add a deadline in the deadlines file or update it if it exists."""
if not self.exchange_root: # Non-FS based exchange
return

deadline = self._format_deadline(deadline)

dir_name = os.path.join(self.exchange_root, self.coursedir.course_id)
file_path = os.path.join(dir_name, self.coursedir.deadline_file)
deadlines = self._read_deadlines(file_path)
deadlines[assignment_id] = deadline

self._write_deadlines(file_path, deadlines)

def _write_deadlines(self, file_path, deadlines):
"""Write the deadlines to the deadlines file and sets access permissions."""
try:
json.dump(deadlines, open(file_path, "w"))
except (FileNotFoundError, PermissionError) as e:
self.log.error("The path to file does not exist or not permitted: %s", e)
return
except IsADirectoryError:
self.log.error("The path to file is a directory: %s", file_path)
return
except:
self.log.error("An unexpected error occurred while writing deadlines")
return

access_mode = 0o664 if self.coursedir.groupshared else 0o644
st_mode = os.stat(file_path).st_mode
if st_mode & access_mode != access_mode:
try:
os.chmod(file_path, (st_mode | access_mode) & 0o777)
except PermissionError:
self.log.warning("Could not update permissions of %s", file_path)
except:
self.log.error("An unexpected error occurred while updating permissions of %s", file_path)

def _format_deadline(self, deadline):
"""Format the deadline."""

return utils.parse_utc(deadline) \
.replace(tzinfo=ZoneInfo("UTC")) \
.isoformat()

def _read_deadlines(self, file_path):
"""Read the deadlines from the deadlines file."""

try:
entries = json.load(open(file_path, "r"))
except (FileNotFoundError, PermissionError):
self.log.warning("No deadlines file found or accessible at {}".format(file_path))
return {}
except json.JSONDecodeError:
self.log.warning("Invalid JSON in deadlines file at {}".format(file_path))
return {}
except:
self.log.error("An unexpected error occurred while loading deadlines")
return {}
return entries
87 changes: 74 additions & 13 deletions src/assignment_list/assignmentlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PageConfig } from '@jupyterlab/coreutils';
import * as React from 'react';

import { showNbGraderDialog, validate } from '../common/validate';
import { showDialog } from '@jupyterlab/apputils';

export class AssignmentList {

Expand Down Expand Up @@ -226,7 +227,7 @@ class Assignment {

private make_link(): HTMLSpanElement {
var container = document.createElement('span');;
container.classList.add('item_name', 'col-sm-6');
container.classList.add('item_name', 'col-sm-4');

var link;
if (this.data['status'] === 'fetched') {
Expand Down Expand Up @@ -259,12 +260,50 @@ class Assignment {
buttons: [Dialog.okButton()]
}, true);

};

private static async proceedLateSubmissionWarning(): Promise<any> {

return showDialog({
title: 'Late Submission',
body: 'The assignment is past due date. Would you like to proceed?',
host: document.body,
buttons: [
{
label: 'cancel',
caption: 'cancel submission',
className: '',
accept: false,
displayType: 'default',
ariaLabel: '',
iconClass: 'closeIcon',
iconLabel: 'Cancel',
actions: []
},
{
label: 'submit',
caption: 'proceed',
className: '',
accept: true,
displayType: 'default',
ariaLabel: '',
iconClass: 'checkIcon',
iconLabel: 'Submit',
actions: []
},
],
defaultButton: 0,
focusNodeSelector: '.my-input',
hasClose: false,
renderer: undefined
}).then((result => {
return result.button.accept;
}));
};

private make_button(): HTMLSpanElement{
private async make_button(): Promise<HTMLSpanElement>{
var container = document.createElement('span');
container.classList.add('item_status', 'col-sm-4')
container.classList.add('item_status', 'col-sm-2')
var button = document.createElement('button');
button.classList.add('btn', 'btn-primary', 'btn-xs')
container.append(button);
Expand Down Expand Up @@ -294,21 +333,38 @@ class Assignment {
}
} else if (this.data.status == 'fetched') {
button.innerText = "Submit";
button.onclick = async function(){
let pastDueDate = false;
if (!!this.data['due_date']) {
var due_date = new Date(this.data['due_date']).getTime();
var now = new Date().getTime();
pastDueDate = (due_date < now)
}
if (pastDueDate) {
button.innerText = 'Late Submit';
}
button.onclick = async function() {
if (pastDueDate) {
const proceed = await Assignment.proceedLateSubmissionWarning();
if (!proceed) {
return;
}
}

button.innerText = 'submitting...';
button.setAttribute('disabled', 'disabled');

const dataToSend = { course_id: that.data['course_id'], assignment_id: that.data['assignment_id']};
try {
const reply = await requestAPI<any>('assignments/submit', {
body: JSON.stringify(dataToSend),
method: 'POST'
});

if(!reply.success){
if (!reply.success) {
that.submit_error(reply);
button.innerText = 'Submit'
button.removeAttribute('disabled')
}else{
} else {
that.on_refresh(reply);
}

Expand All @@ -319,10 +375,8 @@ class Assignment {
`Error on POST /assignment_list/assignments/submit ${dataToSend}.\n${reason}`
);
}

}


} else if (this.data.status == 'submitted') {
button.innerText = "Fetch Feedback";
button.onclick = async function(){
Expand Down Expand Up @@ -351,11 +405,17 @@ class Assignment {
return container;
};

private make_row(): void {
private async make_row(): Promise<void> {
var row = document.createElement('div');
row.classList.add('col-md-12');
row.classList.add('col-md-14');
var link = this.make_link();
row.append(link);
var dl = document.createElement('span');
dl.classList.add('item_deadline', 'col-sm-4')
dl.innerText = !!this.data['due_date']
? new Date(this.data['due_date']).toLocaleString().replace(',', '')
: '';
row.append(dl)
var s = document.createElement('span');
s.classList.add('item_course', 'col-sm-2')
s.innerText = this.data['course_id']
Expand Down Expand Up @@ -396,7 +456,8 @@ class Assignment {
}
}

row.append(this.make_button());
const btn = await this.make_button();
row.append(btn);
this.element.innerHTML= ''

this.element.append(row);
Expand All @@ -409,7 +470,7 @@ const remove_children = function (element: HTMLElement) {
element.innerHTML = '';
}

class Submission{
class Submission {

element: any;
data: any;
Expand Down Expand Up @@ -498,7 +559,7 @@ class Notebook{
private make_button(): HTMLSpanElement {
var that = this;
var container = document.createElement('span');
container.classList.add('item_status', 'col-sm-4');
container.classList.add('item_status', 'col-sm-2');
var button = document.createElement('button')
button.classList.add('btn', 'btn-default', 'btn-xs')

Expand Down
2 changes: 1 addition & 1 deletion style/course_list.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
padding-right: 0;
}

#courses .list_item .item_name, #courses .list_item .item_course{
#courses .list_item .item_name, #courses .list_item .item_course .item_deadline{
display: inline-block;
width: 16.66666667%;
}
Expand Down
Loading