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

Grant Extension interface in the ManageSubmissions section #1906

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
8 changes: 5 additions & 3 deletions nbgrader/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,8 @@ def to_dict(self):
"max_written_score": self.max_written_score,
"task_score": self.task_score,
"max_task_score": self.max_task_score,
"needs_manual_grade": self.needs_manual_grade
"needs_manual_grade": self.needs_manual_grade,
"extension": self.extension.total_seconds() if self.extension is not None else None,
}

def __repr__(self) -> str:
Expand Down Expand Up @@ -3116,7 +3117,8 @@ def submission_dicts(self, assignment_id):
func.coalesce(written_scores.c.max_written_score, 0.0),
func.coalesce(task_scores.c.task_score, 0.0),
func.coalesce(task_scores.c.max_task_score, 0.0),
_manual_grade
_manual_grade,
SubmittedAssignment.extension
).select_from(SubmittedAssignment
).join(SubmittedNotebook).join(Assignment).join(Student).join(Grade)\
.outerjoin(code_scores, SubmittedAssignment.id == code_scores.c.id)\
Expand Down Expand Up @@ -3145,7 +3147,7 @@ def submission_dicts(self, assignment_id):
"score", "max_score", "code_score", "max_code_score",
"written_score", "max_written_score",
"task_score", "max_task_score",
"needs_manual_grade"
"needs_manual_grade", "extension"
]
return [dict(zip(keys, x)) for x in assignments]

Expand Down
42 changes: 41 additions & 1 deletion nbgrader/apps/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import glob
import re
import sys
Expand All @@ -11,7 +12,7 @@
from ..coursedir import CourseDirectory
from ..converters import GenerateAssignment, Autograde, GenerateFeedback, GenerateSolution
from ..exchange import ExchangeFactory, ExchangeError
from ..api import MissingEntry, Gradebook, Student, SubmittedAssignment
from ..api import MissingEntry, InvalidEntry, Gradebook, Student, SubmittedAssignment
from ..utils import parse_utc, temp_attrs, capture_log, as_timezone, to_numeric_tz
from ..auth import Authenticator

Expand Down Expand Up @@ -525,6 +526,7 @@ def get_submission(self, assignment_id, student_id, ungraded=None, students=None
"autograded": False,
"submitted": True,
"student": student_id,
"extension": None,
}

if student_id not in students:
Expand Down Expand Up @@ -569,6 +571,7 @@ def get_submission(self, assignment_id, student_id, ungraded=None, students=None
"autograded": False,
"submitted": False,
"student": student_id,
"extension": None,
}

if student_id not in students:
Expand Down Expand Up @@ -614,6 +617,8 @@ def get_submissions(self, assignment_id):
submission["display_timestamp"] = None
submission["autograded"] = True
submission["submitted"] = True
if submission["extension"]:
submission["extension"] = submission["extension"].total_seconds()
submissions.append(submission)

for student_id in ungraded:
Expand Down Expand Up @@ -1159,3 +1164,38 @@ def fetch_feedback(self, assignment_id, student_id):
assignments = lister_rel.start()
ret_dic["value"] = sorted(assignments, key=lambda x: (x['course_id'], x['assignment_id']))
return ret_dic

def grant_extension_to_student(self, assignment_id, student_id, minutes, hours, days, weeks):
"""Grants extension for a particular assignment and student.

Arguments
---------
assignment_id: string
The name of the assignment
student_id: string
The unique id of the student
minutes: int
The number of minutes to extend the assignment deadline
hours: int
The number of hours to extend the assignment deadline
days: int
The number of days to extend the assignment deadline
weeks: int
The number of weeks to extend the assignment deadline

Returns
-------
result: dict
A dictionary with the following keys (error and log may or may not be present):

- success (bool): whether or not the operation completed successfully
- error (string): formatted traceback
- log (string): captured log output

"""
with self.gradebook as gb:
try:
gb.grant_extension(assignment_id, student_id, minutes, hours, days, weeks)
except InvalidEntry as e:
return {"success": False, "error": str(e)}
return {"success": True}
17 changes: 17 additions & 0 deletions nbgrader/server_extensions/formgrader/apihandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,22 @@ def post(self, assignment_id, student_id):
self.write(json.dumps(self.api.autograde(assignment_id, student_id)))


class ExtensionHandler(BaseApiHandler):
@web.authenticated
@check_xsrf
@check_notebook_dir
def post(self, assignment_id, student_id):
data = self.get_json_body()
try:
minutes = int(data.get('minutes', 0))
hours = int(data.get('hours', 0))
days = int(data.get('days', 0))
weeks = int(data.get('weeks', 0))
except ValueError:
raise web.HTTPError(400, "Invalid extension time")
self.write(json.dumps(self.api.grant_extension_to_student(assignment_id, student_id, minutes, hours, days, weeks)))


class GenerateAllFeedbackHandler(BaseApiHandler):
@web.authenticated
@check_xsrf
Expand Down Expand Up @@ -330,6 +346,7 @@ def post(self, assignment_id, student_id):
(r"/formgrader/api/submissions/([^/]+)", SubmissionCollectionHandler),
(r"/formgrader/api/submission/([^/]+)/([^/]+)", SubmissionHandler),
(r"/formgrader/api/submission/([^/]+)/([^/]+)/autograde", AutogradeHandler),
(r"/formgrader/api/submission/extension/([^/]+)/([^/]+)", ExtensionHandler),

(r"/formgrader/api/submitted_notebooks/([^/]+)/([^/]+)", SubmittedNotebookCollectionHandler),
(r"/formgrader/api/submitted_notebook/([^/]+)/flag", FlagSubmissionHandler),
Expand Down
145 changes: 145 additions & 0 deletions nbgrader/server_extensions/formgrader/static/js/manage_submissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ var SubmissionUI = Backbone.View.extend({
this.$autograde = this.$el.find(".autograde");
this.$generate_feedback = this.$el.find(".generate-feedback");
this.$release_feedback = this.$el.find(".release-feedback");
this.$grant_extension = this.$el.find(".grant-extension");

this.$modal = undefined;
this.$modal_save = undefined;

this.listenTo(this.model, "sync", this.render);

Expand All @@ -36,6 +40,7 @@ var SubmissionUI = Backbone.View.extend({
this.$autograde.empty();
this.$generate_feedback.empty();
this.$release_feedback.empty();
this.$grant_extension.empty();
},

render: function () {
Expand Down Expand Up @@ -128,6 +133,14 @@ var SubmissionUI = Backbone.View.extend({
.append($("<span/>")
.addClass("glyphicon glyphicon-envelope")
.attr("aria-hidden", "true")));

// grant extension
this.$grant_extension.append($("<a/>")
.attr("href", "#")
.click(_.bind(this.grant_extension, this))
.append($("<span/>")
.addClass("glyphicon glyphicon-calendar")
.attr("aria-hidden", "true")));
},

autograde: function () {
Expand Down Expand Up @@ -256,6 +269,137 @@ var SubmissionUI = Backbone.View.extend({
"There was an error releasing feedback for '" + assignment + "' for student '" + student + "'.");
},

grant_extension: function () {
if (!this.model.get("autograded")) {
createModal(
"error-modal",
"Error",
"Extensions cannot be granted for ungraded submissions.");
return;
}
this.openModal();
},

openModal: function () {
var body = $("<table/>").addClass("table table-striped form-table");
var tableBody = $("<tbody/>");
body.append(tableBody);

var weeks = $("<tr/>");
tableBody.append(weeks);
weeks.append($("<td/>").addClass("align-middle").text("Weeks"));
weeks.append($("<td/>").append($("<input/>").addClass("modal-weeks").attr({type: "number", min: 0, step: 1, value: this.modal_extension_weeks})));

var days = $("<tr/>");
tableBody.append(days);
days.append($("<td/>").addClass("align-middle").text("Days"));
days.append($("<td/>").append($("<input/>").addClass("modal-days").attr({type: "number", min: 0, step: 1, value: this.modal_extension_days})));

var hours = $("<tr/>");
tableBody.append(hours);
hours.append($("<td/>").addClass("align-middle").text("Hours"));
hours.append($("<td/>").append($("<input/>").addClass("modal-hours").attr({type: "number", min: 0, step: 1, value: this.modal_extension_hours})));

var minutes = $("<tr/>");
tableBody.append(minutes);
minutes.append($("<td/>").addClass("align-middle").text("Minutes"));
minutes.append($("<td/>").append($("<input/>").addClass("modal-minutes").attr({type: "number", min: 0, step: 1, value: this.modal_extension_minutes})));

var footer = $("<div/>");
footer.append($("<button/>")
.addClass("btn btn-primary save")
.attr("type", "button")
.text("Save"));
footer.append($("<button/>")
.addClass("btn btn-danger")
.attr("type", "button")
.attr("data-dismiss", "modal")
.text("Cancel"));

this.$modal = createModal("grant-extension-modal", "Granting Extension to " + this.model.get("student"), body, footer);

var extension = this.model.get("extension") || 0;
var modal_extension_days = Math.trunc(extension / 86400);
var modal_extension_weeks = Math.trunc(modal_extension_days / 7);
modal_extension_days = modal_extension_days % 7;
var modal_extension_hours = Math.trunc((extension % 86400) / 3600);
var modal_extension_minutes = Math.trunc((extension % 3600) / 60);

this.$modal.find("input.modal-weeks").val(modal_extension_weeks);
this.$modal.find("input.modal-days").val(modal_extension_days);
this.$modal.find("input.modal-hours").val(modal_extension_hours);
this.$modal.find("input.modal-minutes").val(modal_extension_minutes);
this.$modal.find("button.save").click(_.bind(this.save, this));
},

save: function () {
this.animateSaving();

var weeks = this.$modal.find("input.modal-weeks").val() || 0;
var days = this.$modal.find("input.modal-days").val() || 0;
var hours = this.$modal.find("input.modal-hours").val() || 0;
var minutes = this.$modal.find("input.modal-minutes").val() || 0;

this.closeModal();
let student = this.model.get("student");
let assignment = this.model.get("name");

$.ajax({
url: base_url + '/formgrader/api/submission/extension/' + assignment + '/' + student,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
minutes: minutes,
hours: hours,
days: days,
weeks: weeks,
}),
})
.done(_.bind(this.grant_extension_log, this));
},

grant_extension_log: function (response) {
this.model.fetch();
response = JSON.parse(response);
var student = this.model.get("student");
var assignment = this.model.get("name");
if (response["success"]) {
createLogModal(
"success-modal",
"Success",
"Successfully granted extension to '" + assignment + "' for student '" + student + "'.",
response["log"]);

} else {
createLogModal(
"error-modal",
"Error",
"There was an error granting extension to '" + assignment + "' for student '" + student + "':",
response["log"],
response["error"]);
}
},

animateSaving: function () {
if (this.$modal_save) {
this.$modal_save.text("Saving...");
}
},

closeModal: function () {
if (this.$modal) {
this.$modal.modal('hide')
this.$modal = undefined;
this.modal_extension_weeks = 0;
this.modal_extension_days = 0;
this.modal_extension_hours = 0;
this.modal_extension_minutes = 0;
this.$modal_save = undefined;
}

this.render();
},

});

var insertRow = function (table) {
Expand All @@ -268,6 +412,7 @@ var insertRow = function (table) {
row.append($("<td/>").addClass("text-center autograde"));
row.append($("<td/>").addClass("text-center generate-feedback"));
row.append($("<td/>").addClass("text-center release-feedback"));
row.append($("<td/>").addClass("text-center grant-extension"));
table.append(row)
return row;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ nbgrader autograde "{{ assignment_id }}"</pre>
<th class="text-center no-sort">Autograde</th>
<th class="text-center no-sort">Generate Feedback</th>
<th class="text-center no-sort">Release Feedback</th>
<th class="text-center no-sort">Grant Extension</th>
</tr>
{%- endblock -%}

Expand All @@ -77,5 +78,6 @@ nbgrader autograde "{{ assignment_id }}"</pre>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{%- endblock -%}
Loading
Loading