Skip to content
This repository was archived by the owner on May 25, 2025. It is now read-only.

Commit 1b9045e

Browse files
authored
Auto-create appropriate scheduled runs on creation (#170)
* auto create extension run on extension creation * fix formatting * fix * datastructs * ? * prevent extensions from crashing * add print * get run config at ext creation time * add warning * also create the scheduled run for a new assignment * store default run ID in the assignment * try this * fix * load the final grading run config when creating an extension * require grading time config to create an assignment * help * icon * update words * support * help * save start * hold defaults * Revert "hold defaults" This reverts commit f536823. * test * fix * help * reorder * default to * ? * Revert "?" This reverts commit 3ef6f83. * help * help * delete extension run when deleting an extension * fix * fix 3 * reload on closing extensions page * use jquery * fix * fix 3? * sigh * remove print statements
1 parent 1dbdeda commit 1b9045e

File tree

9 files changed

+411
-229
lines changed

9 files changed

+411
-229
lines changed

src/common.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
from pytz import utc
44

55
from config import TZ
6-
from src import db, util
6+
from src import db, sched_api, util
7+
8+
def wrap_delete_scheduled_run(cid, aid, run_id):
9+
sched_run = db.get_scheduled_run(cid, aid, run_id)
10+
if sched_run is None:
11+
raise Exception("Cannot find scheduled run")
12+
sched_api.delete_scheduled_run(sched_run["scheduled_run_id"])
13+
if not db.delete_scheduled_run(cid, aid, run_id):
14+
raise Exception("Failed to delete scheduled run.")
15+
return True
716

817
def in_grading_period(assignment, now=None):
918
if now is None:

src/db.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from bson.objectid import ObjectId
33
from bson.errors import InvalidId
44
from src import util
5+
from src.common import wrap_delete_scheduled_run
56
from src.sched_api import ScheduledRunStatus
67

78
mongo = PyMongo()
@@ -63,6 +64,10 @@ def get_course(cid):
6364
def add_staff_to_course(cid, new_staff_id):
6465
return mongo.db.courses.update({"_id": cid}, {"$set" : {f"staff.{new_staff_id}": {"is_admin": False}}})
6566

67+
68+
def set_templates_for_course(cid, assignment_config: str, grading_config: str):
69+
return mongo.db.courses.update({"_id": cid}, {"$set" : {"default_assignment_config": assignment_config, "default_grading_config": grading_config}})
70+
6671
def remove_staff_from_course(cid, staff_id):
6772
return mongo.db.courses.update({"_id": cid},{"$unset" : {f"staff.{staff_id}": 1}})
6873

@@ -131,7 +136,6 @@ def get_assignments_for_course(cid, visible_only=False):
131136
def get_assignment(cid, aid):
132137
return mongo.db.assignments.find_one({"course_id": cid, "assignment_id": aid})
133138

134-
135139
def add_assignment(cid, aid, max_runs, quota, start, end, visibility):
136140
"""
137141
Add a new assignment.
@@ -235,10 +239,26 @@ def get_extensions(cid, aid, netid=None):
235239
return mongo.db.extensions.find({"course_id": cid, "assignment_id": aid, "netid": netid})
236240

237241

238-
def add_extension(cid, aid, netid, max_runs, start, end):
239-
return mongo.db.extensions.insert_one({"course_id": cid, "assignment_id": aid, "netid": netid, "max_runs": max_runs, "remaining_runs": max_runs, "start": start, "end": end})
242+
def pair_assignment_final_grading_run(cid: str, aid: str, scheduled_run_id: ObjectId):
243+
return mongo.db.assignments.update(
244+
{"course_id": cid, "assignment_id": aid},
245+
{"$set": {"final_grading_run_id": str(scheduled_run_id)}}
246+
)
247+
248+
def add_extension(cid, aid, netid, max_runs, start, end, scheduled_run_id: ObjectId = None):
249+
return mongo.db.extensions.insert_one({"course_id": cid, "assignment_id": aid, "netid": netid, "max_runs": max_runs, "remaining_runs": max_runs, "start": start, "end": end, "run_id": None or str(scheduled_run_id)})
240250

241-
def delete_extension(extension_id):
251+
def delete_extension(cid, aid, extension_id):
252+
document = mongo.db.extensions.find_one({"_id": ObjectId(extension_id)})
253+
if not document:
254+
return None
255+
run_id = document.get("run_id", None)
256+
if run_id:
257+
try:
258+
wrap_delete_scheduled_run(cid, aid, run_id)
259+
except Exception as e:
260+
print(f"Failed to delete scheduled run for extension id {extension_id} run id {run_id}: ", str(e), flush=True)
261+
pass
242262
try:
243263
return mongo.db.extensions.delete_one({"_id": ObjectId(extension_id)})
244264
except InvalidId:

src/routes_admin.py

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import csv
2-
from xxlimited import new
2+
from datetime import timedelta
33
from flask import render_template, abort, request, jsonify
44
from http import HTTPStatus
5-
import json, re
6-
5+
import json
76
from src import db, util, auth, bw_api, sched_api
7+
from src.common import wrap_delete_scheduled_run
88
from src.common import verify_staff, verify_admin, verify_student
99

1010
MIN_PREDEADLINE_RUNS = 1 # Minimum pre-deadline runs for every assignment
11-
11+
1212
class AdminRoutes:
1313
def __init__(self, blueprint):
1414
def none_modified(result):
@@ -33,7 +33,7 @@ def get_course_staff_roster(netid, cid):
3333

3434
staff = course["staff"]
3535
# get all admin_ids by filtering out all non-admins
36-
admin = dict(filter(lambda x : x[1].get("is_admin") == True, staff.items()))
36+
admin = dict(filter(lambda x : x[1].get("is_admin"), staff.items()))
3737
admin = list(admin.keys())
3838
# get the entire staff
3939
total_staff = list(staff.keys())
@@ -144,12 +144,38 @@ def upload_roster_file(netid, cid):
144144
return util.error("The new roster is the same as the current one.")
145145
return util.success("Successfully updated roster.")
146146

147+
@blueprint.route("/staff/course/<cid>/set_template_configs/", methods=["POST"])
148+
@auth.require_auth
149+
@auth.require_admin_status
150+
def set_template_values(netid, cid):
151+
if not db.get_course(cid):
152+
return abort(HTTPStatus.NOT_FOUND)
153+
154+
missing = util.check_missing_fields(request.form,
155+
*["config", "grading_config"])
156+
if missing:
157+
return util.error(f"Missing fields ({', '.join(missing)}).")
158+
try:
159+
json.loads(request.form["config"])
160+
except Exception:
161+
return util.error("Failed to decode student job JSON.")
162+
try:
163+
json.loads(request.form["grading_config"])
164+
except Exception:
165+
return util.error("Failed to decode grading job JSON.")
166+
try:
167+
db.set_templates_for_course(cid, request.form['config'], request.form['grading_config'])
168+
except Exception:
169+
return util.error("Failed to save template to database.")
170+
return util.success("")
171+
172+
147173
@blueprint.route("/staff/course/<cid>/add_assignment/", methods=["POST"])
148174
@auth.require_auth
149175
@auth.require_admin_status
150176
def add_assignment(netid, cid):
151177
missing = util.check_missing_fields(request.form,
152-
*["aid", "max_runs", "quota", "start", "end", "config", "visibility"])
178+
*["aid", "max_runs", "quota", "start", "end", "config", "visibility", "grading_config"])
153179
if missing:
154180
return util.error(f"Missing fields ({', '.join(missing)}).")
155181

@@ -174,6 +200,9 @@ def add_assignment(netid, cid):
174200

175201
start = util.parse_form_datetime(request.form["start"]).timestamp()
176202
end = util.parse_form_datetime(request.form["end"]).timestamp()
203+
run_start_str = (util.parse_form_datetime(request.form["end"]) + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M")
204+
end_str = util.parse_form_datetime(request.form["end"]).strftime("%Y-%m-%dT%H:%M")
205+
run_id = db.generate_new_id()
177206
if start is None or end is None:
178207
return util.error("Missing or invalid Start or End.")
179208
if start >= end:
@@ -191,6 +220,9 @@ def add_assignment(netid, cid):
191220
visibility = request.form["visibility"]
192221

193222
db.add_assignment(cid, aid, max_runs, quota, start, end, visibility)
223+
# Schedule Final Grading Run
224+
add_or_edit_scheduled_run(cid, aid, run_id, {"run_time": run_start_str, "due_time": end_str, "name": "Final Grading Run", "config": request.form['grading_config']}, None)
225+
db.pair_assignment_final_grading_run(cid, aid, str(run_id))
194226
return util.success("")
195227

196228
@blueprint.route("/staff/course/<cid>/<aid>/edit/", methods=["POST"])
@@ -288,15 +320,20 @@ def staff_add_extension(netid, cid, aid):
288320
return util.error("Start must be before End.")
289321

290322
for student_netid in student_netids:
291-
db.add_extension(cid, aid, student_netid, max_runs, start, end)
323+
end_str = util.parse_form_datetime(request.form["end"]).strftime("%Y-%m-%dT%H:%M")
324+
# avoid that weird race condition - start run 5 min after, but with a container due date of the original time
325+
ext_end = (util.parse_form_datetime(request.form["end"]) + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M")
326+
run_id = db.generate_new_id()
327+
add_or_edit_scheduled_run(cid, aid, run_id, {"run_time": ext_end, "due_time": end_str, "name": f"Extension Run - {student_netid}", "config": request.form['config'], "roster": student_netid}, None)
328+
db.add_extension(cid, aid, student_netid, max_runs, start, end, run_id)
292329
return util.success("")
293330

294331
@blueprint.route("/staff/course/<cid>/<aid>/extensions/", methods=["DELETE"])
295332
@auth.require_auth
296333
@auth.require_admin_status
297334
def staff_delete_extension(netid, cid, aid):
298335
extension_id = request.form["_id"]
299-
delete_result = db.delete_extension(extension_id)
336+
delete_result = db.delete_extension(cid, aid, extension_id)
300337

301338
if delete_result is None:
302339
return util.error("Invalid extension, please refresh the page.")
@@ -313,26 +350,26 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id):
313350
return abort(HTTPStatus.NOT_FOUND)
314351

315352
# form validation
316-
missing = util.check_missing_fields(request.form, "run_time", "due_time", "name", "config")
353+
missing = util.check_missing_fields(form, "run_time", "due_time", "name", "config")
317354
if missing:
318355
return util.error(f"Missing fields ({', '.join(missing)}).")
319-
run_time = util.parse_form_datetime(request.form["run_time"]).timestamp()
356+
run_time = util.parse_form_datetime(form["run_time"]).timestamp()
320357
if run_time is None:
321358
return util.error("Missing or invalid run time.")
322359
if run_time <= util.now_timestamp():
323360
return util.error("Run time must be in the future.")
324-
due_time = util.parse_form_datetime(request.form["due_time"]).timestamp()
361+
due_time = util.parse_form_datetime(form["due_time"]).timestamp()
325362
if due_time is None:
326363
return util.error("Missing or invalid due time.")
327-
if "roster" not in request.form or not request.form["roster"]:
364+
if "roster" not in form or not form["roster"]:
328365
roster = None
329366
else:
330-
roster = request.form["roster"].replace(" ", "").lower().split(",")
367+
roster = form["roster"].replace(" ", "").lower().split(",")
331368
for student_netid in roster:
332369
if not util.valid_id(student_netid) or not verify_student(student_netid, cid):
333370
return util.error(f"Invalid or non-existent student NetID: {student_netid}")
334371
try:
335-
config = json.loads(request.form["config"])
372+
config = json.loads(form["config"])
336373
msg = bw_api.set_assignment_config(cid, f"{aid}_{run_id}", config)
337374
if msg:
338375
return util.error(f"Failed to upload config to Broadway: {msg}")
@@ -351,7 +388,7 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id):
351388

352389
assert scheduled_run_id is not None
353390

354-
if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, request.form["name"], scheduled_run_id):
391+
if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, form["name"], scheduled_run_id):
355392
return util.error("Failed to save the changes, please try again.")
356393
return util.success("")
357394

@@ -389,13 +426,8 @@ def staff_get_scheduled_run(netid, cid, aid, run_id):
389426
@auth.require_auth
390427
@auth.require_admin_status
391428
def staff_delete_scheduled_run(netid, cid, aid, run_id):
392-
sched_run = db.get_scheduled_run(cid, aid, run_id)
393-
if sched_run is None:
394-
return util.error("Cannot find scheduled run")
395-
sched_api.delete_scheduled_run(sched_run["scheduled_run_id"])
396-
if not db.delete_scheduled_run(cid, aid, run_id):
397-
return util.error("Failed to delete scheduled run. Please try again")
398-
return util.success("")
399-
400-
401-
429+
try:
430+
wrap_delete_scheduled_run(cid, aid, run_id)
431+
return util.success("")
432+
except Exception as e:
433+
return util.error(str(e))

src/routes_staff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def staff_get_assignment(netid, cid, aid):
6969
student_runs = list(db.get_assignment_runs(cid, aid))
7070
scheduled_runs = list(db.get_scheduled_runs(cid, aid))
7171
is_admin = verify_admin(netid, cid)
72-
72+
assignment['end_plus_one_minute'] = assignment['end'] + 60
7373
return render_template("staff/assignment.html", netid=netid, course=course,
7474
assignment=assignment, student_runs=student_runs,
7575
scheduled_runs=scheduled_runs, sched_run_status=sched_api.ScheduledRunStatus,

src/sched_api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def update_scheduled_run(scheduled_run_id, time):
3939
data = {
4040
"time": util.timestamp_to_iso(time),
4141
}
42-
print(util.timestamp_to_iso(time))
4342
resp = requests.post(url=url, data=data)
4443
is_success = resp.status_code == HTTPStatus.OK
4544
if not is_success:

src/static/modify_assignment.1.0.1.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,69 @@ function modifyAssignment(formName, postUrl, errorSelector, saveBtn, saveBtnIcon
4040
},
4141
visibility: "required",
4242
config: "required",
43+
grading_config: "required",
44+
},
45+
errorPlacement: function(error, element) {
46+
if (element.attr("type") == "radio") {
47+
// radio are grouped by divs, so insert at the end of one level above
48+
let parent = element.parent().parent();
49+
parent.append("<br/>");
50+
parent.append(error);
51+
} else {
52+
// normally insert right after input element
53+
error.insertAfter(element);
54+
}
55+
},
56+
submitHandler: function(form, event) {
57+
event.preventDefault();
58+
// raw data has the form: [{name: "name of input field", value: "input value"}, ...]
59+
let rawData = $(form).serializeArray();
60+
let formData = {};
61+
for (let i = 0; i < rawData.length; i++) {
62+
let fieldName = rawData[i].name;
63+
let fieldVal = rawData[i].value;
64+
// Convert date string to special format
65+
if (fieldName == "start" || fieldName == "end") {
66+
fieldVal = moment(fieldVal).format("YYYY-MM-DDTHH:mm");
67+
}
68+
formData[fieldName] = fieldVal;
69+
}
70+
$.ajax({
71+
type: "POST",
72+
url: postUrl,
73+
data: formData,
74+
beforeSend: function () {
75+
$(errorSelector).html('').fadeOut();
76+
$(saveBtnIcon).addClass("fa-spin");
77+
$(saveBtn).prop('disabled', true);
78+
},
79+
success: function () {
80+
$(modalSelector).modal('hide');
81+
location.reload();
82+
},
83+
error: function (xhr) {
84+
if (!xhr.responseText) {
85+
$(errorSelector).html("Save failed. Please try again.").fadeIn();
86+
} else {
87+
$(errorSelector).html(xhr.responseText).fadeIn();
88+
}
89+
}
90+
}).always(() => {
91+
$(saveBtnIcon).removeClass("fa-spin");
92+
$(saveBtn).prop('disabled', false);
93+
});
94+
}
95+
});
96+
}
97+
98+
function modifyTemplates(formName, postUrl, errorSelector, saveBtn, saveBtnIcon, modalSelector) {
99+
// form validation
100+
$(`form[name="${formName}"]`).validate({
101+
errorClass: "text-danger",
102+
errorElement: "small",
103+
rules: {
104+
config: "required",
105+
grading_config: "required",
43106
},
44107
errorPlacement: function(error, element) {
45108
if (element.attr("type") == "radio") {

0 commit comments

Comments
 (0)