From 6ca4b7d69f3265210e9d91cee7f267a21a6e81a0 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Thu, 25 Jan 2024 13:14:16 -0600 Subject: [PATCH 01/23] Non-existent courses now return a 404, as opposed to error-ing the server --- src/routes_staff.py | 4 +++- src/routes_student.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes_staff.py b/src/routes_staff.py index 185ba53..430ee8a 100644 --- a/src/routes_staff.py +++ b/src/routes_staff.py @@ -46,10 +46,12 @@ def staff_home(netid): @blueprint.route("/staff/course//", methods=["GET"]) @auth.require_auth def staff_get_course(netid, cid): + course = db.get_course(cid) + if course is None: + return abort(HTTPStatus.NOT_FOUND) if not verify_staff(netid, cid): return abort(HTTPStatus.FORBIDDEN) - course = db.get_course(cid) assignments = db.get_assignments_for_course(cid) is_admin = verify_admin(netid, cid) now = util.now_timestamp() diff --git a/src/routes_student.py b/src/routes_student.py index 2858603..1936f37 100644 --- a/src/routes_student.py +++ b/src/routes_student.py @@ -22,10 +22,12 @@ def student_home(netid): @util.disable_in_maintenance_mode @auth.require_auth def student_get_course(netid, cid): + course = db.get_course(cid) + if course is None: + return abort(HTTPStatus.NOT_FOUND) if not verify_student_or_staff(netid, cid): return abort(HTTPStatus.FORBIDDEN) - course = db.get_course(cid) if verify_staff(netid, cid): assignments = db.get_assignments_for_course(cid) else: From 899a187793ca93f622f9c8e44d324dbc44caa741 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 3 Feb 2024 19:30:49 -0600 Subject: [PATCH 02/23] Updated .gitignore --- .gitignore | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 67be4a1..cbb1522 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,13 @@ config.py .idea/ -.vscode/ \ No newline at end of file +.vscode/ + +# Miscellanous local stuff +db/ + +misc/ + +requirements.txt + +.gitignore From 5a8237f7496fe54cf88fa19203b7913346c58e7c Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 3 Feb 2024 19:32:03 -0600 Subject: [PATCH 03/23] Recached files --- .gitignore | 119 ----------------------------------------------- requirements.txt | 19 -------- 2 files changed, 138 deletions(-) delete mode 100644 .gitignore delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cbb1522..0000000 --- a/.gitignore +++ /dev/null @@ -1,119 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -config.py - -.idea/ - -.vscode/ - -# Miscellanous local stuff -db/ - -misc/ - -requirements.txt - -.gitignore diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 90903b2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -certifi==2018.11.29 -chardet==3.0.4 -Click==7.0 -Flask==1.0.2 -Flask-PyMongo==2.2.0 -Flask-Session==0.3.1 -humanize==0.5.1 -idna==2.8 -itsdangerous==1.1.0 -Jinja2==2.11.3 -MarkupSafe==1.1.0 -pymongo==3.7.2 -pytz==2018.9 -requests==2.21.0 -urllib3==1.24.2 -werkzeug==0.15.5 -ansi2html==1.5.2 -gunicorn==20.0.4 -identity==0.3.2 \ No newline at end of file From 67c55525c703953d5e5a7258bc6ed25146790633 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Mon, 5 Feb 2024 02:05:08 -0600 Subject: [PATCH 04/23] Updated on-demand admin to handle API requests --- src/auth.py | 15 ++++ src/routes_admin.py | 193 +++++++++++++++++++++++++------------------- 2 files changed, 127 insertions(+), 81 deletions(-) diff --git a/src/auth.py b/src/auth.py index fda5825..44c72bc 100644 --- a/src/auth.py +++ b/src/auth.py @@ -105,6 +105,21 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper +def require_auth_or_course_token(func): + """ + A route decorator that merges the functionalities of `require_auth` and `require_course_auth`. + Useful for overlapping functionalities of two routes where one requires API auth and the other + requires non-api auth. + """ + @wraps(func) + def wrapper(*args, **kwargs): + if '/api' in request.path: + return require_course_auth(func) + else: + return require_auth(func) + return wrapper + + def begin_login(): """ Render the login page for the user using external IAM service diff --git a/src/routes_admin.py b/src/routes_admin.py index c73e46d..9625237 100644 --- a/src/routes_admin.py +++ b/src/routes_admin.py @@ -1,5 +1,6 @@ from flask import render_template, abort, request, jsonify from http import HTTPStatus +from functools import wraps import json, re from src import db, util, auth, bw_api, sched_api @@ -7,9 +8,9 @@ MIN_PREDEADLINE_RUNS = 1 # Minimum pre-deadline runs for every assignment - class AdminRoutes: def __init__(self, blueprint): + def none_modified(result): """ Return true if the database was NOT modified as a result of the API call. @@ -24,8 +25,9 @@ def get_course_roster_page(netid, cid): course = db.get_course(cid) return render_template("staff/roster.html", netid=netid, course=course) - @blueprint.route("/staff/course//staff_roster", methods=["GET"]) - @auth.require_auth + @blueprint.route("/api/staff/course//get_staff_roster", methods=["GET"], api=True) + @blueprint.route("/staff/course//staff_roster", methods=["GET"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def get_course_staff_roster(netid, cid): course = db.get_course(cid) @@ -39,18 +41,21 @@ def get_course_staff_roster(netid, cid): return jsonify(admin_ids=admin, staff_ids=total_staff, user=netid) - @blueprint.route("/staff/course//student_roster", methods=["GET"]) - @auth.require_auth + @blueprint.route("/api/staff/course//get_student_roster", methods=["GET"], api=True) + @blueprint.route("/staff/course//student_roster", methods=["GET"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def get_course_student_roster(netid, cid): course = db.get_course(cid) return jsonify(course['student_ids']) - @blueprint.route("/staff/course//add_staff", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//add_staff", methods=["POST"], api=True) + @blueprint.route("/staff/course//add_staff", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def add_course_staff(netid, cid): - new_staff_id = request.form.get('netid').lower() + form = request.json if request.api else request.form + new_staff_id = form.get('netid').lower() if new_staff_id is None: return util.error("Cannot find netid field") if not util.is_valid_netid(new_staff_id): @@ -60,21 +65,25 @@ def add_course_staff(netid, cid): return util.error(f"'{new_staff_id}' is already a course staff") return util.success(f"Successfully added {new_staff_id}") - @blueprint.route("/staff/course//remove_staff", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//remove_staff", methods=["POST"], api=True) + @blueprint.route("/staff/course//remove_staff", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def remove_course_staff(netid, cid): - staff_id = request.form.get('netid') + form = request.json if request.api else request.form + staff_id = form.get('netid') result = db.remove_staff_from_course(cid, staff_id) if none_modified(result): return util.error(f"'{staff_id}' is not a staff") return util.success(f"Successfully removed '{staff_id}'") - @blueprint.route("/staff/course//promote_staff", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//promote_staff", methods=["POST"], api=True) + @blueprint.route("/staff/course//promote_staff", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def promote_course_staff(netid, cid): - staff_id = request.form.get('netid') + form = request.json if request.api else request.form + staff_id = form.get('netid') if not verify_staff(staff_id, cid): return util.error(f"'{staff_id}' is not a staff") result = db.add_admin_to_course(cid, staff_id) @@ -82,21 +91,25 @@ def promote_course_staff(netid, cid): return util.error(f"'{staff_id}' is already an admin") return util.success(f"Successfully made '{staff_id}' admin") - @blueprint.route("/staff/course//demote_admin", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//demote_admin", methods=["POST"], api=True) + @blueprint.route("/staff/course//demote_admin", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def demote_course_admin(netid, cid): - staff_id = request.form.get('netid') + form = request.json if request.api else request.form + staff_id = form.get('netid') if not verify_staff(staff_id, cid) or not verify_admin(staff_id, cid): return util.error(f"'{staff_id}' is not a admin") db.remove_admin_from_course(cid, staff_id) return util.success(f"Successfully removed '{staff_id}' from admin") - @blueprint.route("/staff/course//add_student", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//add_student", methods=["POST"], api=True) + @blueprint.route("/staff/course//add_student", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def add_course_student(netid, cid): - new_student_id = request.form.get('netid').lower() + form = request.json if request.api else request.form + new_student_id = form.get('netid').lower() if new_student_id is None: return util.error("Cannot find netid field") if not util.is_valid_netid(new_student_id): @@ -106,21 +119,24 @@ def add_course_student(netid, cid): return util.error(f"'{new_student_id}' is already a student") return util.success(f"Successfully added {new_student_id}") - @blueprint.route("/staff/course//remove_student", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//remove_student", methods=["POST"], api=True) + @blueprint.route("/staff/course//remove_student", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def remove_course_student(netid, cid): - student_id = request.form.get('netid') + form = request.json if request.api else request.form + student_id = form.get('netid') result = db.remove_student_from_course(cid, student_id) if none_modified(result): return util.error(f"'{student_id}' is not a student") return util.success(f"Successfully removed '{student_id}'") - @blueprint.route("/staff/course//upload_roster_file", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course//upload_roster_file", methods=["POST"], api=True) + @blueprint.route("/staff/course//upload_roster_file", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def upload_roster_file(netid, cid): - file_content = request.form.get('content') + file_content = request.form.get('content') if request.api else request.json["roster"] netids = file_content.strip().lower().split('\n') for i, student_id in enumerate(netids): if not util.is_valid_netid(student_id): @@ -130,17 +146,19 @@ def upload_roster_file(netid, cid): if none_modified(result): return util.error("The new roster is the same as the current one.") return util.success("Successfully updated roster.") - - @blueprint.route("/staff/course//add_assignment/", methods=["POST"]) - @auth.require_auth + + @blueprint.route("/api/staff/course//add_assignment", methods=["POST"], api=True) + @blueprint.route("/staff/course//add_assignment/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def add_assignment(netid, cid): - missing = util.check_missing_fields(request.form, + form = request.json if request.api else request.form + missing = util.check_missing_fields(form, *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) if missing: return util.error(f"Missing fields ({', '.join(missing)}).") - aid = request.form["aid"] + aid = form["aid"] if not util.valid_id(aid): return util.error("Invalid Assignment ID. Allowed characters: a-z A-Z _ - .") @@ -149,25 +167,25 @@ def add_assignment(netid, cid): return util.error("Assignment ID already exists.") try: - max_runs = int(request.form["max_runs"]) + max_runs = int(form["max_runs"]) if max_runs < MIN_PREDEADLINE_RUNS: return util.error(f"Max Runs must be at least {MIN_PREDEADLINE_RUNS}.") except ValueError: return util.error("Max Runs must be a positive integer.") - quota = request.form["quota"] + quota = form["quota"] if not db.Quota.is_valid(quota): return util.error("Quota Type is invalid.") - start = util.parse_form_datetime(request.form["start"]).timestamp() - end = util.parse_form_datetime(request.form["end"]).timestamp() + start = util.parse_form_datetime(form["start"]).timestamp() + end = util.parse_form_datetime(form["end"]).timestamp() if start is None or end is None: return util.error("Missing or invalid Start or End.") if start >= end: return util.error("Start must be before End.") try: - config = json.loads(request.form["config"]) + config = json.loads(form["config"]) msg = bw_api.set_assignment_config(cid, aid, config) if msg: @@ -175,47 +193,50 @@ def add_assignment(netid, cid): except json.decoder.JSONDecodeError: return util.error("Failed to decode config JSON") - visibility = request.form["visibility"] + visibility = form["visibility"] db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) return util.success("") - @blueprint.route("/staff/course///edit/", methods=["POST"]) - @auth.require_auth + + @blueprint.route("/api/staff/course///edit_assignment", methods=["POST"], api=True) + @blueprint.route("/staff/course///edit/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def edit_assignment(netid, cid, aid): + form = request.json if request.api else request.form course = db.get_course(cid) assignment = db.get_assignment(cid, aid) if course is None or assignment is None: return abort(HTTPStatus.NOT_FOUND) - missing = util.check_missing_fields(request.form, *["max_runs", "quota", "start", "end", "visibility"]) + missing = util.check_missing_fields(form, *["max_runs", "quota", "start", "end", "visibility"]) if missing: return util.error(f"Missing fields ({', '.join(missing)}).") try: - max_runs = int(request.form["max_runs"]) + max_runs = int(form["max_runs"]) if max_runs < MIN_PREDEADLINE_RUNS: return util.error(f"Max Runs must be at least {MIN_PREDEADLINE_RUNS}.") except ValueError: return util.error("Max Runs must be a positive integer.") - quota = request.form["quota"] + quota = form["quota"] if not db.Quota.is_valid(quota): return util.error("Quota Type is invalid.") - start = util.parse_form_datetime(request.form["start"]).timestamp() - end = util.parse_form_datetime(request.form["end"]).timestamp() + start = util.parse_form_datetime(form["start"]).timestamp() + end = util.parse_form_datetime(form["end"]).timestamp() if start is None or end is None: return util.error("Missing or invalid Start or End.") if start >= end: return util.error("Start must be before End.") try: - config_str = request.form.get("config") + config_str = form.get("config") if config_str is not None: # skip update otherwise - config = json.loads(request.form["config"]) + config = json.loads(form["config"]) msg = bw_api.set_assignment_config(cid, aid, config) if msg: @@ -223,22 +244,24 @@ def edit_assignment(netid, cid, aid): except json.decoder.JSONDecodeError: return util.error("Failed to decode config JSON") - visibility = request.form["visibility"] + visibility = form["visibility"] if not db.update_assignment(cid, aid, max_runs, quota, start, end, visibility): return util.error("Save failed or no changes were made.") return util.success("") - @blueprint.route("/staff/course///delete/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course///delete_assignment", methods=["POST"], api=True) + @blueprint.route("/staff/course///delete/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def delete_assignment(netid, cid, aid): if not db.remove_assignment(cid, aid): return util.error("Assignment doesn't exist") return util.success("") - @blueprint.route("/staff/course///extensions/", methods=["GET"]) - @auth.require_auth + @blueprint.route("/api/staff/course///get_extensions", methods=["GET"], api=True) + @blueprint.route("/staff/course///extensions/", methods=["GET"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_get_extensions(netid, cid, aid): extensions = list(db.get_extensions(cid, aid)) @@ -246,31 +269,33 @@ def staff_get_extensions(netid, cid, aid): ext["_id"] = str(ext["_id"]) return util.success(jsonify(extensions), HTTPStatus.OK) - @blueprint.route("/staff/course///extensions/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course///add_extension", methods=["POST"], api=True) + @blueprint.route("/staff/course///extensions/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_add_extension(netid, cid, aid): + form = request.json if request.api else request.form assignment = db.get_assignment(cid, aid) if not assignment: return util.error("Invalid course or assignment. Please try again.") - if util.check_missing_fields(request.form, "netids", "max_runs", "start", "end"): + if util.check_missing_fields(form, "netids", "max_runs", "start", "end"): return util.error("Missing fields. Please try again.") - student_netids = request.form["netids"].replace(" ", "").lower().split(",") + student_netids = form["netids"].replace(" ", "").lower().split(",") for student_netid in student_netids: if not util.valid_id(student_netid) or not verify_student(student_netid, cid): return util.error(f"Invalid or non-existent student NetID: {student_netid}") try: - max_runs = int(request.form["max_runs"]) + max_runs = int(form["max_runs"]) if max_runs < 1: return util.error("Max Runs must be a positive integer.") except ValueError: return util.error("Max Runs must be a positive integer.") - start = util.parse_form_datetime(request.form["start"]).timestamp() - end = util.parse_form_datetime(request.form["end"]).timestamp() + start = util.parse_form_datetime(form["start"]).timestamp() + end = util.parse_form_datetime(form["end"]).timestamp() if start >= end: return util.error("Start must be before End.") @@ -278,11 +303,13 @@ def staff_add_extension(netid, cid, aid): db.add_extension(cid, aid, student_netid, max_runs, start, end) return util.success("") - @blueprint.route("/staff/course///extensions/", methods=["DELETE"]) - @auth.require_auth + @blueprint.route("/api/staff/course///delete_extension", methods=["DELETE"], api=True) + @blueprint.route("/staff/course///extensions/", methods=["DELETE"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_delete_extension(netid, cid, aid): - extension_id = request.form["_id"] + form = request.json if request.api else request.form + extension_id = form["_id"] delete_result = db.delete_extension(extension_id) if delete_result is None: @@ -300,26 +327,27 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): return abort(HTTPStatus.NOT_FOUND) # form validation - missing = util.check_missing_fields(request.form, "run_time", "due_time", "name", "config") + missing = util.check_missing_fields(form, "run_time", "due_time", "name", "config") if missing: return util.error(f"Missing fields ({', '.join(missing)}).") - run_time = util.parse_form_datetime(request.form["run_time"]).timestamp() + run_time = util.parse_form_datetime(form["run_time"]).timestamp() if run_time is None: return util.error("Missing or invalid run time.") if run_time <= util.now_timestamp(): return util.error("Run time must be in the future.") - due_time = util.parse_form_datetime(request.form["due_time"]).timestamp() + due_time = util.parse_form_datetime(form["due_time"]).timestamp() if due_time is None: return util.error("Missing or invalid due time.") - if "roster" not in request.form or not request.form["roster"]: + if "roster" not in form or not form["roster"]: roster = None else: - roster = request.form["roster"].replace(" ", "").lower().split(",") + roster = form["roster"].replace(" ", "").lower().split(",") for student_netid in roster: if not util.valid_id(student_netid) or not verify_student(student_netid, cid): return util.error(f"Invalid or non-existent student NetID: {student_netid}") try: - config = json.loads(request.form["config"]) + config = json.loads(form + ["config"]) msg = bw_api.set_assignment_config(cid, f"{aid}_{run_id}", config) if msg: return util.error(f"Failed to upload config to Broadway: {msg}") @@ -338,32 +366,37 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): assert scheduled_run_id is not None - if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, request.form["name"], scheduled_run_id): + if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, form["name"], scheduled_run_id): return util.error("Failed to save the changes, please try again.") return util.success("") - @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course///schedule_run", methods=["POST"], api=True) + @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_schedule_run(netid, cid, aid): + form = request.json if request.api else request.form # generate new id for this scheduled run run_id = db.generate_new_id() - return add_or_edit_scheduled_run(cid, aid, run_id, request.form, None) + return add_or_edit_scheduled_run(cid, aid, run_id, form, None) - @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api/staff/course///schedule_run/", methods=["POST"], api=True) + @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_edit_scheduled_run(netid, cid, aid, run_id): + form = request.json if request.api else request.form sched_run = db.get_scheduled_run(cid, aid, run_id) if sched_run is None: return util.error("Could not find this scheduled run. Please refresh and try again.") if sched_run["status"] != sched_api.ScheduledRunStatus.SCHEDULED: return util.error("Cannot edit past runs") scheduled_run_id = sched_run["scheduled_run_id"] - return add_or_edit_scheduled_run(cid, aid, run_id, request.form, scheduled_run_id) + return add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id) - @blueprint.route("/staff/course///schedule_run/", methods=["GET"]) - @auth.require_auth + @blueprint.route("/api/staff/course///schedule_run/", methods=["GET"], api=True) + @blueprint.route("/staff/course///schedule_run/", methods=["GET"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_get_scheduled_run(netid, cid, aid, run_id): sched_run = db.get_scheduled_run(cid, aid, run_id) @@ -372,8 +405,9 @@ def staff_get_scheduled_run(netid, cid, aid, run_id): del sched_run["_id"] return util.success(json.dumps(sched_run), 200) - @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"]) - @auth.require_auth + @blueprint.route("/api/staff/course///schedule_run/", methods=["DELETE"], api=True) + @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"], api=False) + @auth.require_auth_or_course_token @auth.require_admin_status def staff_delete_scheduled_run(netid, cid, aid, run_id): sched_run = db.get_scheduled_run(cid, aid, run_id) @@ -383,6 +417,3 @@ def staff_delete_scheduled_run(netid, cid, aid, run_id): if not db.delete_scheduled_run(cid, aid, run_id): return util.error("Failed to delete scheduled run. Please try again") return util.success("") - - - \ No newline at end of file From 72fca379e46ee1e3d98bfd41a19bcd149088743a Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Wed, 7 Feb 2024 17:52:37 -0600 Subject: [PATCH 05/23] Changed API routes to be more consistent with existing ones --- src/routes_admin.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/routes_admin.py b/src/routes_admin.py index 9625237..e2fb26f 100644 --- a/src/routes_admin.py +++ b/src/routes_admin.py @@ -25,7 +25,7 @@ def get_course_roster_page(netid, cid): course = db.get_course(cid) return render_template("staff/roster.html", netid=netid, course=course) - @blueprint.route("/api/staff/course//get_staff_roster", methods=["GET"], api=True) + @blueprint.route("/api/course//get_staff_roster", methods=["GET"], api=True) @blueprint.route("/staff/course//staff_roster", methods=["GET"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -41,7 +41,7 @@ def get_course_staff_roster(netid, cid): return jsonify(admin_ids=admin, staff_ids=total_staff, user=netid) - @blueprint.route("/api/staff/course//get_student_roster", methods=["GET"], api=True) + @blueprint.route("/api/course//get_student_roster", methods=["GET"], api=True) @blueprint.route("/staff/course//student_roster", methods=["GET"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -49,7 +49,7 @@ def get_course_student_roster(netid, cid): course = db.get_course(cid) return jsonify(course['student_ids']) - @blueprint.route("/api/staff/course//add_staff", methods=["POST"], api=True) + @blueprint.route("/api/course//add_staff", methods=["POST"], api=True) @blueprint.route("/staff/course//add_staff", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -65,7 +65,7 @@ def add_course_staff(netid, cid): return util.error(f"'{new_staff_id}' is already a course staff") return util.success(f"Successfully added {new_staff_id}") - @blueprint.route("/api/staff/course//remove_staff", methods=["POST"], api=True) + @blueprint.route("/api/course//remove_staff", methods=["POST"], api=True) @blueprint.route("/staff/course//remove_staff", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -77,7 +77,7 @@ def remove_course_staff(netid, cid): return util.error(f"'{staff_id}' is not a staff") return util.success(f"Successfully removed '{staff_id}'") - @blueprint.route("/api/staff/course//promote_staff", methods=["POST"], api=True) + @blueprint.route("/api/course//promote_staff", methods=["POST"], api=True) @blueprint.route("/staff/course//promote_staff", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -91,7 +91,7 @@ def promote_course_staff(netid, cid): return util.error(f"'{staff_id}' is already an admin") return util.success(f"Successfully made '{staff_id}' admin") - @blueprint.route("/api/staff/course//demote_admin", methods=["POST"], api=True) + @blueprint.route("/api/course//demote_admin", methods=["POST"], api=True) @blueprint.route("/staff/course//demote_admin", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -103,7 +103,7 @@ def demote_course_admin(netid, cid): db.remove_admin_from_course(cid, staff_id) return util.success(f"Successfully removed '{staff_id}' from admin") - @blueprint.route("/api/staff/course//add_student", methods=["POST"], api=True) + @blueprint.route("/api/course//add_student", methods=["POST"], api=True) @blueprint.route("/staff/course//add_student", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -119,7 +119,7 @@ def add_course_student(netid, cid): return util.error(f"'{new_student_id}' is already a student") return util.success(f"Successfully added {new_student_id}") - @blueprint.route("/api/staff/course//remove_student", methods=["POST"], api=True) + @blueprint.route("/api/course//remove_student", methods=["POST"], api=True) @blueprint.route("/staff/course//remove_student", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -131,7 +131,7 @@ def remove_course_student(netid, cid): return util.error(f"'{student_id}' is not a student") return util.success(f"Successfully removed '{student_id}'") - @blueprint.route("/api/staff/course//upload_roster_file", methods=["POST"], api=True) + @blueprint.route("/api/course//upload_roster_file", methods=["POST"], api=True) @blueprint.route("/staff/course//upload_roster_file", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -147,7 +147,7 @@ def upload_roster_file(netid, cid): return util.error("The new roster is the same as the current one.") return util.success("Successfully updated roster.") - @blueprint.route("/api/staff/course//add_assignment", methods=["POST"], api=True) + @blueprint.route("/api/course//add_assignment", methods=["POST"], api=True) @blueprint.route("/staff/course//add_assignment/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -199,7 +199,7 @@ def add_assignment(netid, cid): return util.success("") - @blueprint.route("/api/staff/course///edit_assignment", methods=["POST"], api=True) + @blueprint.route("/api/course///edit_assignment", methods=["POST"], api=True) @blueprint.route("/staff/course///edit/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -250,7 +250,7 @@ def edit_assignment(netid, cid, aid): return util.error("Save failed or no changes were made.") return util.success("") - @blueprint.route("/api/staff/course///delete_assignment", methods=["POST"], api=True) + @blueprint.route("/api/course///delete_assignment", methods=["POST"], api=True) @blueprint.route("/staff/course///delete/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -259,7 +259,7 @@ def delete_assignment(netid, cid, aid): return util.error("Assignment doesn't exist") return util.success("") - @blueprint.route("/api/staff/course///get_extensions", methods=["GET"], api=True) + @blueprint.route("/api/course///get_extensions", methods=["GET"], api=True) @blueprint.route("/staff/course///extensions/", methods=["GET"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -269,7 +269,7 @@ def staff_get_extensions(netid, cid, aid): ext["_id"] = str(ext["_id"]) return util.success(jsonify(extensions), HTTPStatus.OK) - @blueprint.route("/api/staff/course///add_extension", methods=["POST"], api=True) + @blueprint.route("/api/course///add_extension", methods=["POST"], api=True) @blueprint.route("/staff/course///extensions/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -303,7 +303,7 @@ def staff_add_extension(netid, cid, aid): db.add_extension(cid, aid, student_netid, max_runs, start, end) return util.success("") - @blueprint.route("/api/staff/course///delete_extension", methods=["DELETE"], api=True) + @blueprint.route("/api/course///delete_extension", methods=["DELETE"], api=True) @blueprint.route("/staff/course///extensions/", methods=["DELETE"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -370,7 +370,7 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): return util.error("Failed to save the changes, please try again.") return util.success("") - @blueprint.route("/api/staff/course///schedule_run", methods=["POST"], api=True) + @blueprint.route("/api/course///schedule_run", methods=["POST"], api=True) @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -380,7 +380,7 @@ def staff_schedule_run(netid, cid, aid): run_id = db.generate_new_id() return add_or_edit_scheduled_run(cid, aid, run_id, form, None) - @blueprint.route("/api/staff/course///schedule_run/", methods=["POST"], api=True) + @blueprint.route("/api/course///schedule_run/", methods=["POST"], api=True) @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -394,7 +394,7 @@ def staff_edit_scheduled_run(netid, cid, aid, run_id): scheduled_run_id = sched_run["scheduled_run_id"] return add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id) - @blueprint.route("/api/staff/course///schedule_run/", methods=["GET"], api=True) + @blueprint.route("/api/course///schedule_run/", methods=["GET"], api=True) @blueprint.route("/staff/course///schedule_run/", methods=["GET"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status @@ -405,7 +405,7 @@ def staff_get_scheduled_run(netid, cid, aid, run_id): del sched_run["_id"] return util.success(json.dumps(sched_run), 200) - @blueprint.route("/api/staff/course///schedule_run/", methods=["DELETE"], api=True) + @blueprint.route("/api/course///schedule_run/", methods=["DELETE"], api=True) @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"], api=False) @auth.require_auth_or_course_token @auth.require_admin_status From 1c24a5590589d23fda6614404eb1f6b1aa9b8daa Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Wed, 7 Feb 2024 17:53:00 -0600 Subject: [PATCH 06/23] Added API to add multiple extensions at once --- src/routes_api.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/routes_api.py b/src/routes_api.py index 6118903..e89274d 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -4,6 +4,7 @@ from src import db, util, auth, bw_api from src.sched_api import ScheduledRunStatus +from src.common import verify_student class ApiRoutes: def __init__(self, blueprint): @@ -51,4 +52,43 @@ def trigger_scheduled_run(cid, aid, scheduled_run_id): db.update_scheduled_run_status(sched_run["_id"], ScheduledRunStatus.RAN) db.update_scheduled_run_bw_run_id(sched_run["_id"], bw_run_id) return util.success("") + + # Want to avoid stuff like this, with overlaps in function definitions + # Best way is to consider an AdminOperations class and have AdminRoutes and APIRoutes + # use the functionality defined in there, instead of whatever I did with AdminRoutes currently + @blueprint.route("/api//add_extensions", methods=["POST"]) + @auth.require_course_auth + @auth.require_admin_status + def add_extensions(netid, cid, aid): + assignment = db.get_assignment(cid, aid) + if not assignment: + return util.error("Invalid course or assignment. Please try again.") + + if util.check_missing_fields(request.json, "extensions"): + return util.error("Missing fields. Please try again.") + + for ext_json in request.json: + if util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end"): + return util.error("Missing fields. Please try again.") + student_netids = ext_json["netids"].replace(" ", "").lower().split(",") + for student_netid in student_netids: + if not util.valid_id(student_netid) or not verify_student(student_netid, cid): + return util.error(f"Invalid or non-existent student NetID: {student_netid}") + + try: + max_runs = int(ext_json["max_runs"]) + if max_runs < 1: + return util.error("Max Runs must be a positive integer.") + except ValueError: + return util.error("Max Runs must be a positive integer.") + + start = util.parse_form_datetime(ext_json["start"]).timestamp() + end = util.parse_form_datetime(ext_json["end"]).timestamp() + if start >= end: + return util.error("Start must be before End.") + + for student_netid in student_netids: + db.add_extension(cid, aid, student_netid, max_runs, start, end) + return util.success("") + From 91ae4f2d73e4010baf63210f083649ce8d02a29e Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 17 Feb 2024 12:05:44 -0600 Subject: [PATCH 07/23] Added api to schedule runs --- src/routes_api.py | 172 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/src/routes_api.py b/src/routes_api.py index 6118903..3d9b330 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -1,9 +1,13 @@ import logging from flask import request from http import HTTPStatus +import json from src import db, util, auth, bw_api -from src.sched_api import ScheduledRunStatus +from src.sched_api import ScheduledRunStatus, schedule_run, update_scheduled_run +from src.common import verify_student + +MIN_PREDEADLINE_RUNS = 1 # Minimum pre-deadline runs for every assignment class ApiRoutes: def __init__(self, blueprint): @@ -52,3 +56,169 @@ def trigger_scheduled_run(cid, aid, scheduled_run_id): db.update_scheduled_run_bw_run_id(sched_run["_id"], bw_run_id) return util.success("") + + # Want to avoid stuff like this, with overlaps in function definitions + # Best way is to consider an AdminOperations class and have AdminRoutes and APIRoutes + # use the functionality defined in there, instead of whatever I did with AdminRoutes currently + @blueprint.route("/api//add_extensions", methods=["POST"]) + @auth.require_course_auth + @auth.require_admin_status + def add_extensions(netid, cid, aid): + assignment = db.get_assignment(cid, aid) + if not assignment: + return util.error("Invalid course or assignment. Please try again.") + + if util.check_missing_fields(request.json, "extensions"): + return util.error("Missing fields. Please try again.") + + for ext_json in request.json: + if util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end"): + return util.error("Missing fields. Please try again.") + + student_netids = ext_json["netids"].replace(" ", "").lower().split(",") + for student_netid in student_netids: + if not util.valid_id(student_netid) or not verify_student(student_netid, cid): + return util.error(f"Invalid or non-existent student NetID: {student_netid}") + + try: + max_runs = int(ext_json["max_runs"]) + if max_runs < 1: + return util.error("Max Runs must be a positive integer.") + except ValueError: + return util.error("Max Runs must be a positive integer.") + + start = util.parse_form_datetime(ext_json["start"]).timestamp() + end = util.parse_form_datetime(ext_json["end"]).timestamp() + if start >= end: + return util.error("Start must be before End.") + + for student_netid in student_netids: + db.add_extension(cid, aid, student_netid, max_runs, start, end) + return util.success("") + + @blueprint.route("/api//add_assignment", methods=["POST"]) + @auth.require_course_auth + @auth.require_admin_status + def api_add_assignment(netid, cid): + form = request.json + missing = util.check_missing_fields(form, + *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) + if missing: + return util.error(f"Missing fields ({', '.join(missing)}).") + + aid = form["aid"] + if not util.valid_id(aid): + return util.error("Invalid Assignment ID. Allowed characters: a-z A-Z _ - .") + + new_assignment = db.get_assignment(cid, aid) + if new_assignment: + return util.error("Assignment ID already exists.") + + try: + max_runs = int(form["max_runs"]) + if max_runs < MIN_PREDEADLINE_RUNS: + return util.error(f"Max Runs must be at least {MIN_PREDEADLINE_RUNS}.") + except ValueError: + return util.error("Max Runs must be a positive integer.") + + quota = form["quota"] + if not db.Quota.is_valid(quota): + return util.error("Quota Type is invalid.") + + start = util.parse_form_datetime(form["start"]).timestamp() + end = util.parse_form_datetime(form["end"]).timestamp() + if start is None or end is None: + return util.error("Missing or invalid Start or End.") + if start >= end: + return util.error("Start must be before End.") + + try: + config = json.loads(form["config"]) + msg = bw_api.set_assignment_config(cid, aid, config) + + if msg: + return util.error(f"Failed to add assignment to Broadway: {msg}") + except json.decoder.JSONDecodeError: + return util.error("Failed to decode config JSON") + + visibility = form["visibility"] + + db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) + return util.success("") + + def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): + # course and assignment name validation + course = db.get_course(cid) + assignment = db.get_assignment(cid, aid) + if course is None or assignment is None: + return util.error("Could not find assignment", HTTPStatus.NOT_FOUND) + + # form validation + missing = util.check_missing_fields(form, "run_time", "due_time", "name", "config") + if missing: + return util.error(f"Missing fields ({', '.join(missing)}).") + run_time = util.parse_form_datetime(form["run_time"]).timestamp() + if run_time is None: + return util.error("Missing or invalid run time.") + if run_time <= util.now_timestamp(): + return util.error("Run time must be in the future.") + due_time = util.parse_form_datetime(form["due_time"]).timestamp() + if due_time is None: + return util.error("Missing or invalid due time.") + if "roster" not in form or not form["roster"]: + roster = None + else: + roster = form["roster"].replace(" ", "").lower().split(",") + for student_netid in roster: + if not util.valid_id(student_netid) or not verify_student(student_netid, cid): + return util.error(f"Invalid or non-existent student NetID: {student_netid}") + try: + config = json.loads(form["config"]) + msg = bw_api.set_assignment_config(cid, f"{aid}_{run_id}", config) + if msg: + return util.error(f"Failed to upload config to Broadway: {msg}") + except json.decoder.JSONDecodeError: + return util.error("Failed to decode config JSON") + + # Schedule a new run with scheduler + if scheduled_run_id is None: + scheduled_run_id = schedule_run(run_time, cid, aid) + if scheduled_run_id is None: + return util.error("Failed to schedule run with scheduler") + # Or if the run was already scheduled, update the time + else: + if not update_scheduled_run(scheduled_run_id, run_time): + return util.error("Failed to update scheduled run time with scheduler") + + assert scheduled_run_id is not None + + if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, form["name"], scheduled_run_id): + return util.error("Failed to save the changes, please try again.") + return util.success("") + + @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) + @auth.require_auth + @auth.require_admin_status + def api_add_scheduled_run(netid, cid, aid): + # generate new id for this scheduled run + run_id = db.generate_new_id() + return add_or_edit_scheduled_run(cid, aid, run_id, request.json, None) + + @blueprint.route("/staff/course///schedule_runs/", methods=["POST"]) + @auth.require_auth + @auth.require_admin_status + def api_add_scheduled_runs(netid, cid, aid): + # generate new id for this scheduled run + missing = util.check_missing_fields(request.json, "runs") + if missing: + return util.error(f"Missing fields {', '.join(missing)}") + # TODO: there's probably a better way to do this + if isinstance(request.json["runs"], list): + return util.error("runs field must be a list of run configs!") + for run_config in request.json["runs"]: + run_id = db.generate_new_id() + retval = add_or_edit_scheduled_run(cid, aid, run_id, run_config, None) + # TODO: There should be a distinction between good and bad responses + if retval[1] != HTTPStatus.NO_CONTENT: + return retval + return util.success("") \ No newline at end of file From bd109416f089c9db19281ffb47e0e6fedb189f1b Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 17 Feb 2024 13:59:25 -0600 Subject: [PATCH 08/23] Reverted changes to admin routes --- src/routes_admin.py | 100 ++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/routes_admin.py b/src/routes_admin.py index e2fb26f..fb56774 100644 --- a/src/routes_admin.py +++ b/src/routes_admin.py @@ -25,8 +25,8 @@ def get_course_roster_page(netid, cid): course = db.get_course(cid) return render_template("staff/roster.html", netid=netid, course=course) - @blueprint.route("/api/course//get_staff_roster", methods=["GET"], api=True) - @blueprint.route("/staff/course//staff_roster", methods=["GET"], api=False) + + @blueprint.route("/staff/course//staff_roster", methods=["GET"]) @auth.require_auth_or_course_token @auth.require_admin_status def get_course_staff_roster(netid, cid): @@ -41,20 +41,20 @@ def get_course_staff_roster(netid, cid): return jsonify(admin_ids=admin, staff_ids=total_staff, user=netid) - @blueprint.route("/api/course//get_student_roster", methods=["GET"], api=True) - @blueprint.route("/staff/course//student_roster", methods=["GET"], api=False) + + @blueprint.route("/staff/course//student_roster", methods=["GET"]) @auth.require_auth_or_course_token @auth.require_admin_status def get_course_student_roster(netid, cid): course = db.get_course(cid) return jsonify(course['student_ids']) - @blueprint.route("/api/course//add_staff", methods=["POST"], api=True) - @blueprint.route("/staff/course//add_staff", methods=["POST"], api=False) + + @blueprint.route("/staff/course//add_staff", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def add_course_staff(netid, cid): - form = request.json if request.api else request.form + form = request.form new_staff_id = form.get('netid').lower() if new_staff_id is None: return util.error("Cannot find netid field") @@ -65,24 +65,24 @@ def add_course_staff(netid, cid): return util.error(f"'{new_staff_id}' is already a course staff") return util.success(f"Successfully added {new_staff_id}") - @blueprint.route("/api/course//remove_staff", methods=["POST"], api=True) - @blueprint.route("/staff/course//remove_staff", methods=["POST"], api=False) + + @blueprint.route("/staff/course//remove_staff", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def remove_course_staff(netid, cid): - form = request.json if request.api else request.form + form = request.form staff_id = form.get('netid') result = db.remove_staff_from_course(cid, staff_id) if none_modified(result): return util.error(f"'{staff_id}' is not a staff") return util.success(f"Successfully removed '{staff_id}'") - @blueprint.route("/api/course//promote_staff", methods=["POST"], api=True) - @blueprint.route("/staff/course//promote_staff", methods=["POST"], api=False) + + @blueprint.route("/staff/course//promote_staff", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def promote_course_staff(netid, cid): - form = request.json if request.api else request.form + form = request.form staff_id = form.get('netid') if not verify_staff(staff_id, cid): return util.error(f"'{staff_id}' is not a staff") @@ -91,24 +91,24 @@ def promote_course_staff(netid, cid): return util.error(f"'{staff_id}' is already an admin") return util.success(f"Successfully made '{staff_id}' admin") - @blueprint.route("/api/course//demote_admin", methods=["POST"], api=True) - @blueprint.route("/staff/course//demote_admin", methods=["POST"], api=False) + + @blueprint.route("/staff/course//demote_admin", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def demote_course_admin(netid, cid): - form = request.json if request.api else request.form + form = request.form staff_id = form.get('netid') if not verify_staff(staff_id, cid) or not verify_admin(staff_id, cid): return util.error(f"'{staff_id}' is not a admin") db.remove_admin_from_course(cid, staff_id) return util.success(f"Successfully removed '{staff_id}' from admin") - @blueprint.route("/api/course//add_student", methods=["POST"], api=True) - @blueprint.route("/staff/course//add_student", methods=["POST"], api=False) + + @blueprint.route("/staff/course//add_student", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def add_course_student(netid, cid): - form = request.json if request.api else request.form + form = request.form new_student_id = form.get('netid').lower() if new_student_id is None: return util.error("Cannot find netid field") @@ -119,20 +119,20 @@ def add_course_student(netid, cid): return util.error(f"'{new_student_id}' is already a student") return util.success(f"Successfully added {new_student_id}") - @blueprint.route("/api/course//remove_student", methods=["POST"], api=True) - @blueprint.route("/staff/course//remove_student", methods=["POST"], api=False) + + @blueprint.route("/staff/course//remove_student", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def remove_course_student(netid, cid): - form = request.json if request.api else request.form + form = request.form student_id = form.get('netid') result = db.remove_student_from_course(cid, student_id) if none_modified(result): return util.error(f"'{student_id}' is not a student") return util.success(f"Successfully removed '{student_id}'") - @blueprint.route("/api/course//upload_roster_file", methods=["POST"], api=True) - @blueprint.route("/staff/course//upload_roster_file", methods=["POST"], api=False) + + @blueprint.route("/staff/course//upload_roster_file", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def upload_roster_file(netid, cid): @@ -147,12 +147,12 @@ def upload_roster_file(netid, cid): return util.error("The new roster is the same as the current one.") return util.success("Successfully updated roster.") - @blueprint.route("/api/course//add_assignment", methods=["POST"], api=True) - @blueprint.route("/staff/course//add_assignment/", methods=["POST"], api=False) + + @blueprint.route("/staff/course//add_assignment/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def add_assignment(netid, cid): - form = request.json if request.api else request.form + form = request.form missing = util.check_missing_fields(form, *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) if missing: @@ -199,12 +199,12 @@ def add_assignment(netid, cid): return util.success("") - @blueprint.route("/api/course///edit_assignment", methods=["POST"], api=True) - @blueprint.route("/staff/course///edit/", methods=["POST"], api=False) + + @blueprint.route("/staff/course///edit/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def edit_assignment(netid, cid, aid): - form = request.json if request.api else request.form + form = request.form course = db.get_course(cid) assignment = db.get_assignment(cid, aid) if course is None or assignment is None: @@ -250,8 +250,8 @@ def edit_assignment(netid, cid, aid): return util.error("Save failed or no changes were made.") return util.success("") - @blueprint.route("/api/course///delete_assignment", methods=["POST"], api=True) - @blueprint.route("/staff/course///delete/", methods=["POST"], api=False) + + @blueprint.route("/staff/course///delete/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def delete_assignment(netid, cid, aid): @@ -259,8 +259,8 @@ def delete_assignment(netid, cid, aid): return util.error("Assignment doesn't exist") return util.success("") - @blueprint.route("/api/course///get_extensions", methods=["GET"], api=True) - @blueprint.route("/staff/course///extensions/", methods=["GET"], api=False) + + @blueprint.route("/staff/course///extensions/", methods=["GET"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_get_extensions(netid, cid, aid): @@ -269,12 +269,12 @@ def staff_get_extensions(netid, cid, aid): ext["_id"] = str(ext["_id"]) return util.success(jsonify(extensions), HTTPStatus.OK) - @blueprint.route("/api/course///add_extension", methods=["POST"], api=True) - @blueprint.route("/staff/course///extensions/", methods=["POST"], api=False) + + @blueprint.route("/staff/course///extensions/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_add_extension(netid, cid, aid): - form = request.json if request.api else request.form + form = request.form assignment = db.get_assignment(cid, aid) if not assignment: return util.error("Invalid course or assignment. Please try again.") @@ -303,12 +303,12 @@ def staff_add_extension(netid, cid, aid): db.add_extension(cid, aid, student_netid, max_runs, start, end) return util.success("") - @blueprint.route("/api/course///delete_extension", methods=["DELETE"], api=True) - @blueprint.route("/staff/course///extensions/", methods=["DELETE"], api=False) + + @blueprint.route("/staff/course///extensions/", methods=["DELETE"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_delete_extension(netid, cid, aid): - form = request.json if request.api else request.form + form = request.form extension_id = form["_id"] delete_result = db.delete_extension(extension_id) @@ -370,22 +370,22 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): return util.error("Failed to save the changes, please try again.") return util.success("") - @blueprint.route("/api/course///schedule_run", methods=["POST"], api=True) - @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) + + @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_schedule_run(netid, cid, aid): - form = request.json if request.api else request.form + form = request.form # generate new id for this scheduled run run_id = db.generate_new_id() return add_or_edit_scheduled_run(cid, aid, run_id, form, None) - @blueprint.route("/api/course///schedule_run/", methods=["POST"], api=True) - @blueprint.route("/staff/course///schedule_run/", methods=["POST"], api=False) + + @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_edit_scheduled_run(netid, cid, aid, run_id): - form = request.json if request.api else request.form + form = request.form sched_run = db.get_scheduled_run(cid, aid, run_id) if sched_run is None: return util.error("Could not find this scheduled run. Please refresh and try again.") @@ -394,8 +394,8 @@ def staff_edit_scheduled_run(netid, cid, aid, run_id): scheduled_run_id = sched_run["scheduled_run_id"] return add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id) - @blueprint.route("/api/course///schedule_run/", methods=["GET"], api=True) - @blueprint.route("/staff/course///schedule_run/", methods=["GET"], api=False) + + @blueprint.route("/staff/course///schedule_run/", methods=["GET"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_get_scheduled_run(netid, cid, aid, run_id): @@ -405,8 +405,8 @@ def staff_get_scheduled_run(netid, cid, aid, run_id): del sched_run["_id"] return util.success(json.dumps(sched_run), 200) - @blueprint.route("/api/course///schedule_run/", methods=["DELETE"], api=True) - @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"], api=False) + + @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"]) @auth.require_auth_or_course_token @auth.require_admin_status def staff_delete_scheduled_run(netid, cid, aid, run_id): From 0f89da5e410f655b25a7130dfd31be44fb57433b Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 17 Feb 2024 15:25:45 -0600 Subject: [PATCH 09/23] Fixed duplicate routes --- src/routes_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes_api.py b/src/routes_api.py index 8efc273..ef5deee 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -195,16 +195,16 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): return util.error("Failed to save the changes, please try again.") return util.success("") - @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api///schedule_run/", methods=["POST"]) + @auth.require_course_auth @auth.require_admin_status def api_add_scheduled_run(netid, cid, aid): # generate new id for this scheduled run run_id = db.generate_new_id() return add_or_edit_scheduled_run(cid, aid, run_id, request.json, None) - @blueprint.route("/staff/course///schedule_runs/", methods=["POST"]) - @auth.require_auth + @blueprint.route("/api///schedule_runs/", methods=["POST"]) + @auth.require_course_auth @auth.require_admin_status def api_add_scheduled_runs(netid, cid, aid): # generate new id for this scheduled run From 8c5f3a05e296e3267fda698e9aec5a5e60ea2d74 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 17 Feb 2024 15:35:35 -0600 Subject: [PATCH 10/23] Reverted changes to admin routes --- src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 5555 bytes src/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 3507 bytes src/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 3561 bytes src/__pycache__/auth.cpython-37.pyc | Bin 0 -> 5707 bytes src/__pycache__/bw_api.cpython-37.pyc | Bin 0 -> 4804 bytes src/__pycache__/common.cpython-37.pyc | Bin 0 -> 3631 bytes src/__pycache__/db.cpython-37.pyc | Bin 0 -> 10791 bytes src/__pycache__/ghe_api.cpython-37.pyc | Bin 0 -> 1720 bytes src/__pycache__/routes_admin.cpython-37.pyc | Bin 0 -> 14039 bytes src/__pycache__/routes_api.cpython-37.pyc | Bin 0 -> 2390 bytes src/__pycache__/routes_staff.cpython-37.pyc | Bin 0 -> 5047 bytes src/__pycache__/routes_student.cpython-37.pyc | Bin 0 -> 4330 bytes src/__pycache__/sched_api.cpython-37.pyc | Bin 0 -> 2081 bytes .../template_filters.cpython-37.pyc | Bin 0 -> 1813 bytes src/__pycache__/util.cpython-37.pyc | Bin 0 -> 6105 bytes src/auth.py | 15 -- src/routes_admin.py | 155 +++++++----------- 17 files changed, 62 insertions(+), 108 deletions(-) create mode 100644 src/__pycache__/__init__.cpython-312.pyc create mode 100644 src/__pycache__/__init__.cpython-37.pyc create mode 100644 src/__pycache__/__init__.cpython-38.pyc create mode 100644 src/__pycache__/auth.cpython-37.pyc create mode 100644 src/__pycache__/bw_api.cpython-37.pyc create mode 100644 src/__pycache__/common.cpython-37.pyc create mode 100644 src/__pycache__/db.cpython-37.pyc create mode 100644 src/__pycache__/ghe_api.cpython-37.pyc create mode 100644 src/__pycache__/routes_admin.cpython-37.pyc create mode 100644 src/__pycache__/routes_api.cpython-37.pyc create mode 100644 src/__pycache__/routes_staff.cpython-37.pyc create mode 100644 src/__pycache__/routes_student.cpython-37.pyc create mode 100644 src/__pycache__/sched_api.cpython-37.pyc create mode 100644 src/__pycache__/template_filters.cpython-37.pyc create mode 100644 src/__pycache__/util.cpython-37.pyc diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94e84b2dacb63c3b1347eb7c269d9eee47672608 GIT binary patch literal 5555 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 RUnc|4NOZ;ZHxgq$@^4ZW+1mgB literal 0 HcmV?d00001 diff --git a/src/__pycache__/__init__.cpython-37.pyc b/src/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0559eb88fdd15fd0778efe40f390d066ed73c7ff GIT binary patch literal 3507 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/common.cpython-37.pyc b/src/__pycache__/common.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2db4e0fbc1cb20e1ef4031f64f0cbcc2c9e6dc0f GIT binary patch literal 3631 zcmcgvO>Y}T7~a`0uN^y0-R}=zg$l)mCRDTtl@eN-OQp&PwHJJ-wehUeZT2Ib9jA@8 zIi(Wi07sAzLJc?m1;1mioH%pi#PiO^cAT0jy>zXad3WBKoq3-3W5!=K8Z`sYPmMpr zBXfrF7rl%g3&Ks5{6|#W;4C&0qhm0V&6p)-$JDYFTZ!GVwQR>u;&$Am(y8G6EO&Y3 z8S8l5U@D8Mcd#_ehlqAKh95}J;G1&Q)n0X zX?_OnQSRI`T5tW0JsB;gJRZnUMo9?S8ps|R^M0pgDwD4&dmy8@-7{${9%>2HZIt|D zR0ZS4x+fN-@q+Cd+pGY{Gw8K3Y@KD7@NS-gbPrRRfn}|>a&j36sjM*NEmOGoifW|U zlP>K6s$>Ie*MdBc*3u+QF%Zf>3{l6xf3Gb)%#v^^${!B`5%Q&8{>jxVORFLa_>*Ak za+Y4^VG^W#DHpw^UX~lwKMHyw%X& zEvoc6VpGlQX=B&cUZ>uf?$jUa_A}AV<$zNFINxYc&i$|N-@25N2f)C!LzMzg|b993j>|(tH`&aZJrmqSCQq z*3BBT&3SWyon%zRC1_@QP8*v4^BnCT@Ez%BOEYf5(zdp=BVAeH7J7`4?F*-H`(!M; zJ=>-}mMXK)bHuLBK4)iSA3>fiDEjxHBM&{5BL*nP2%eGet4)yl15|7Kgw3*@LMrDR0*7}?-M3@g2EdK`WaaO=!v-y34kiGz+$aEP_zPVNpiSDO#tO4MZO1Q-NWxW?76g zgkY)4-iQMfsv45sUL-AHT&`0bTv&Yi+FKg#)T*~1hP`$F$-_`S426%%UxQbqemj(R zR{SXU10RQ2zwakuvWgK|-w&i8hsfUk6~El$Vy$|8BM?F2>j1ruIfFbD7xVHfq!ytZ zh?F#hF$_|FGl&NvR3lb2`_rsoS#cGp_9zk8ARnZ|({x}YDN8b3kntIjL3W{sTB7MT zN>27E#wQ>h@9Qr4@$lzElA)8BAEo=qIy4O>OGs$ML^;d9v+`XQm{?%*pUvc7;kCX0v~leLJK(RFIu zEKJVUtJz1O%+xnU7qXTLxLmd@@99bJdFSKtvlbqq*NHRW%!)3Z-=4VmWaYw4N1+(UQZmn6X LdahT~zq$Va@OWrm literal 0 HcmV?d00001 diff --git a/src/__pycache__/db.cpython-37.pyc b/src/__pycache__/db.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee6cfcc8e1d48bd2a8fcc514ff00b905b9889d27 GIT binary patch literal 10791 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/routes_admin.cpython-37.pyc b/src/__pycache__/routes_admin.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce4352d73e3627556a4fd7c32555e81d5fc31699 GIT binary patch literal 14039 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 LrVwuw4a@u=jn#?V literal 0 HcmV?d00001 diff --git a/src/__pycache__/routes_api.cpython-37.pyc b/src/__pycache__/routes_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76ad574b54b2bbc093c93568c952b6be9d2a42ea GIT binary patch literal 2390 zcmah~&2JM&6rb7IU9UfqLZBsq3bwRW>I!V3R;oZz1SL=qRX{OqC9CafvoogDK)qG!9Upv{XYd*e8FoE3o=Y zf6NDg#Z4gbJxml)oS?4$w-TIMT?^|qNl0pUZL_u$Cw04STIp6&uj{4NZq@8NNiD5+ z>ll55s7os^5Uo77x($jusOh~1-DtasRfV&s1rrjMwR?{rKkCR(7NTh@i|#7Bka42y zu#kP_?j8i;AO^XWPSj_#NEqEIvQj?M>gL0O#Z4e_7AA(en4+#lF|}TxE};a>+p2nN z5bxxLWFpeyde}e?&}|@b7p4*3hv=!*!<77HNxZL#P@6h0&~vPRW6Q|6M`UbTpAX%N%MiA!|stU4oT9J+YrKzPWw5{BkkGM9x^eE4Ha_H~nJPoB})Zb?E z{+6F-eiCQQf34jfk{kZ|8@xXJuv0`46JoDO;7l1X^-FE7<}%kASN6WhGvx@$>P$$}I17$dSl=_zfC<^F)(Wm+-)i7tdjY>z z5X4z5gP^sM3{)&L{>l_!M%Y-DWd!LG9gccJ7? z$vY?9HRu(f^LMaR9U(xgJ+jTtuSYb)jafruC)3*C#MqIwNnQH|Se;pTiC&QrpqEzH z(9|BgBiCs8+xStNVz7%QjnW(N5fFUTlViMP!$XtzO6ie1lRZ_&*{p95qa7Bp_!)qF zmScaBR3CEe&pMXA3~xC)`;cyb{Zp~-?-tUJ!Ys?BzZYlJj{vPq4DyV|Sx@U|DWqIs zTHXJe(`Jromi<^5NBW`gOC`qa75|_gM|~f-+Vc9s>-Cl*pn+=`s`^{V!xadWM++9n zIAz0+?}jlnRbS?dz;(C>vF!VE{5MaR*Y2YplgFVK>>e&QI9OAaB=7YgUR5~=c?Jt} znfV9c1lOHSRXexu-P!u?+dEr|KpRjstux9CE*RHfRi`01E`uHqb>4J_)rHyRTEu4Z zUM4}gcW>P{+IDVjtBWT}!Xr`^im!r&W`&>Eh6vm!b?UfI*r}QwW7nPeb-_6PR*P;K zrf={|n)CjNxAGjY#OE*}vWz|4fG)6%&*K`=D-WnfJaXBrphG~HIE9DT{;NZrXr<{W z$MhraYFEx^n8uvVG8&ji^z3}I>@l;vEkwm3JW8n%g=T3Bmh7qOZc?xTk2A@A(96Hl z(>q~%^CngWrqC@9L4-D5sW0Q<%D*Go2JLVr3sV*Zsul!kuHSQ5H-Z5AZc=ji=5brb zs?Ialb~J{Beo8!SzMD(@Gx$=@UJ{BYvqz>c<)C~32=r@%-4cd6MHcD4F^@C?3)%9#xt3dw%D2S>B literal 0 HcmV?d00001 diff --git a/src/__pycache__/routes_staff.cpython-37.pyc b/src/__pycache__/routes_staff.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..058eacad0168f651e1dcbdfb65a6207c84dbbe0c GIT binary patch literal 5047 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/sched_api.cpython-37.pyc b/src/__pycache__/sched_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b38a84d6fe05bd78ee8d6449086dc559b276c450 GIT binary patch literal 2081 zcmb7FOK;pZ5Ee;E`|z$|yDrcaeXv1$SirG?6etQKD3Zo@V>ECJKYGwbU`4K1TdlMq zsX8{~Q*zC%w>E%%?6p5f*Pi+pdg=_dyS9s7%7tb*Lk?#&-^bC{{eIVjG5!1xb=2~_ zKT(+v29w9|m?;S9Ns@U{^G_lII?d>WMU0@0W&R|H0yp=waMFre#QVmRfec@JGJHj& zwqoOsY{~X(?-hx3`hP)G?_fY`zqDxv|J`TD$KM}WVM_yA z|LEzn-JO%?yNB_~;eLXf=YzoHF+Aoy5X!@3BPvP6B#nH@qCol(AgI@ll97^SrsQFn zHwQ-@Gq*)di@XkpB83eHgRpMLaV{n*j_Yn5PYQH|c`uG%lp>q0bmm#@MC|a%K@{#i z*?+#f^Sh^)AQvb8ez-L%CTc4+;HQ<``cW4`o{94ZMgBmliOA)a(aDxeBo@-`Fly``7Tk}Fzy*Q6p9mGn1u>037TD^@`* z-q6dS3a)}{Px|*ftcqBL@^v|kd3Ctpy3Dz-+(6#krW3X4Hf&FY787UK=EtLEiQ9tD zYsf7a(KpR`nWrz^R%69vdfPuqQX9ond3#0RiZCWU%_l0ii&d>`sq^hc9N7es z8(csv$%lnrgtj9{{7mt2Y2XFs;BNB$A$OthO~FfP4NgUpKpGpo3BV7EeD;6r$pbluwN}bid6jszLi{R*u%b`ZZGzC$w;8CtLu`S}% z6m?sx7bTcX?N1A1YkzEt9DMq?ok7uR2KCV^c*H8&e67s%hG)8m#@;+C21ScO;dQ&T zO(<#8Wuo5)J;5Y!+~H9KhK5HKu<0$k1pYWSk!z9?U{=E|&|VSZS$`ZSO*SkSLgw~ zDe%SpJ=Y76;j{DR#dQSS=AW5C?*>!-4wSDBraH~7UWd7U7Y$2*GPJNbDWuYPeQWyN zw{ZJv|*U%F|zsW@3L$_{f;@Qjm8};=jzj%=!qR+v}AYHt0 znZl1OlOFUDV8DH_CD=7_ASV}{a(DJDwIc`6Y5KFmyF2SF|AnD!j`%+q4u~t-ZRBRB+)2YB z*J+SLpz5&Mr9S}0VWdCA*+(coM!`W0{019$0}&DZ35rFdZ!X|^bJSfZ9>T`vB8CXU z42Dnf(0&uerNQ7k(6k4CWbL$JOPCb-Fg?_3HQKL=TE}tG<4wl!K*Zu)@AQ>3| literal 0 HcmV?d00001 diff --git a/src/__pycache__/template_filters.cpython-37.pyc b/src/__pycache__/template_filters.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c5e843c5624d4457f33508e5796ab200428ff7f GIT binary patch literal 1813 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 h/staff_roster", methods=["GET"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def get_course_staff_roster(netid, cid): course = db.get_course(cid) @@ -41,21 +39,18 @@ def get_course_staff_roster(netid, cid): return jsonify(admin_ids=admin, staff_ids=total_staff, user=netid) - @blueprint.route("/staff/course//student_roster", methods=["GET"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def get_course_student_roster(netid, cid): course = db.get_course(cid) return jsonify(course['student_ids']) - @blueprint.route("/staff/course//add_staff", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def add_course_staff(netid, cid): - form = request.form - new_staff_id = form.get('netid').lower() + new_staff_id = request.form.get('netid').lower() if new_staff_id is None: return util.error("Cannot find netid field") if not util.is_valid_netid(new_staff_id): @@ -65,25 +60,21 @@ def add_course_staff(netid, cid): return util.error(f"'{new_staff_id}' is already a course staff") return util.success(f"Successfully added {new_staff_id}") - @blueprint.route("/staff/course//remove_staff", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def remove_course_staff(netid, cid): - form = request.form - staff_id = form.get('netid') + staff_id = request.form.get('netid') result = db.remove_staff_from_course(cid, staff_id) if none_modified(result): return util.error(f"'{staff_id}' is not a staff") return util.success(f"Successfully removed '{staff_id}'") - @blueprint.route("/staff/course//promote_staff", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def promote_course_staff(netid, cid): - form = request.form - staff_id = form.get('netid') + staff_id = request.form.get('netid') if not verify_staff(staff_id, cid): return util.error(f"'{staff_id}' is not a staff") result = db.add_admin_to_course(cid, staff_id) @@ -91,25 +82,21 @@ def promote_course_staff(netid, cid): return util.error(f"'{staff_id}' is already an admin") return util.success(f"Successfully made '{staff_id}' admin") - @blueprint.route("/staff/course//demote_admin", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def demote_course_admin(netid, cid): - form = request.form - staff_id = form.get('netid') + staff_id = request.form.get('netid') if not verify_staff(staff_id, cid) or not verify_admin(staff_id, cid): return util.error(f"'{staff_id}' is not a admin") db.remove_admin_from_course(cid, staff_id) return util.success(f"Successfully removed '{staff_id}' from admin") - @blueprint.route("/staff/course//add_student", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def add_course_student(netid, cid): - form = request.form - new_student_id = form.get('netid').lower() + new_student_id = request.form.get('netid').lower() if new_student_id is None: return util.error("Cannot find netid field") if not util.is_valid_netid(new_student_id): @@ -119,24 +106,21 @@ def add_course_student(netid, cid): return util.error(f"'{new_student_id}' is already a student") return util.success(f"Successfully added {new_student_id}") - @blueprint.route("/staff/course//remove_student", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def remove_course_student(netid, cid): - form = request.form - student_id = form.get('netid') + student_id = request.form.get('netid') result = db.remove_student_from_course(cid, student_id) if none_modified(result): return util.error(f"'{student_id}' is not a student") return util.success(f"Successfully removed '{student_id}'") - @blueprint.route("/staff/course//upload_roster_file", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def upload_roster_file(netid, cid): - file_content = request.form.get('content') if request.api else request.json["roster"] + file_content = request.form.get('content') netids = file_content.strip().lower().split('\n') for i, student_id in enumerate(netids): if not util.is_valid_netid(student_id): @@ -146,19 +130,17 @@ def upload_roster_file(netid, cid): if none_modified(result): return util.error("The new roster is the same as the current one.") return util.success("Successfully updated roster.") - - + @blueprint.route("/staff/course//add_assignment/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def add_assignment(netid, cid): - form = request.form - missing = util.check_missing_fields(form, + missing = util.check_missing_fields(request.form, *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) if missing: return util.error(f"Missing fields ({', '.join(missing)}).") - aid = form["aid"] + aid = request.form["aid"] if not util.valid_id(aid): return util.error("Invalid Assignment ID. Allowed characters: a-z A-Z _ - .") @@ -167,25 +149,25 @@ def add_assignment(netid, cid): return util.error("Assignment ID already exists.") try: - max_runs = int(form["max_runs"]) + max_runs = int(request.form["max_runs"]) if max_runs < MIN_PREDEADLINE_RUNS: return util.error(f"Max Runs must be at least {MIN_PREDEADLINE_RUNS}.") except ValueError: return util.error("Max Runs must be a positive integer.") - quota = form["quota"] + quota = request.form["quota"] if not db.Quota.is_valid(quota): return util.error("Quota Type is invalid.") - start = util.parse_form_datetime(form["start"]).timestamp() - end = util.parse_form_datetime(form["end"]).timestamp() + start = util.parse_form_datetime(request.form["start"]).timestamp() + end = util.parse_form_datetime(request.form["end"]).timestamp() if start is None or end is None: return util.error("Missing or invalid Start or End.") if start >= end: return util.error("Start must be before End.") try: - config = json.loads(form["config"]) + config = json.loads(request.form["config"]) msg = bw_api.set_assignment_config(cid, aid, config) if msg: @@ -193,50 +175,47 @@ def add_assignment(netid, cid): except json.decoder.JSONDecodeError: return util.error("Failed to decode config JSON") - visibility = form["visibility"] + visibility = request.form["visibility"] db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) return util.success("") - - @blueprint.route("/staff/course///edit/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def edit_assignment(netid, cid, aid): - form = request.form course = db.get_course(cid) assignment = db.get_assignment(cid, aid) if course is None or assignment is None: return abort(HTTPStatus.NOT_FOUND) - missing = util.check_missing_fields(form, *["max_runs", "quota", "start", "end", "visibility"]) + missing = util.check_missing_fields(request.form, *["max_runs", "quota", "start", "end", "visibility"]) if missing: return util.error(f"Missing fields ({', '.join(missing)}).") try: - max_runs = int(form["max_runs"]) + max_runs = int(request.form["max_runs"]) if max_runs < MIN_PREDEADLINE_RUNS: return util.error(f"Max Runs must be at least {MIN_PREDEADLINE_RUNS}.") except ValueError: return util.error("Max Runs must be a positive integer.") - quota = form["quota"] + quota = request.form["quota"] if not db.Quota.is_valid(quota): return util.error("Quota Type is invalid.") - start = util.parse_form_datetime(form["start"]).timestamp() - end = util.parse_form_datetime(form["end"]).timestamp() + start = util.parse_form_datetime(request.form["start"]).timestamp() + end = util.parse_form_datetime(request.form["end"]).timestamp() if start is None or end is None: return util.error("Missing or invalid Start or End.") if start >= end: return util.error("Start must be before End.") try: - config_str = form.get("config") + config_str = request.form.get("config") if config_str is not None: # skip update otherwise - config = json.loads(form["config"]) + config = json.loads(request.form["config"]) msg = bw_api.set_assignment_config(cid, aid, config) if msg: @@ -244,24 +223,22 @@ def edit_assignment(netid, cid, aid): except json.decoder.JSONDecodeError: return util.error("Failed to decode config JSON") - visibility = form["visibility"] + visibility = request.form["visibility"] if not db.update_assignment(cid, aid, max_runs, quota, start, end, visibility): return util.error("Save failed or no changes were made.") return util.success("") - @blueprint.route("/staff/course///delete/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def delete_assignment(netid, cid, aid): if not db.remove_assignment(cid, aid): return util.error("Assignment doesn't exist") return util.success("") - @blueprint.route("/staff/course///extensions/", methods=["GET"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_get_extensions(netid, cid, aid): extensions = list(db.get_extensions(cid, aid)) @@ -269,33 +246,31 @@ def staff_get_extensions(netid, cid, aid): ext["_id"] = str(ext["_id"]) return util.success(jsonify(extensions), HTTPStatus.OK) - @blueprint.route("/staff/course///extensions/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_add_extension(netid, cid, aid): - form = request.form assignment = db.get_assignment(cid, aid) if not assignment: return util.error("Invalid course or assignment. Please try again.") - if util.check_missing_fields(form, "netids", "max_runs", "start", "end"): + if util.check_missing_fields(request.form, "netids", "max_runs", "start", "end"): return util.error("Missing fields. Please try again.") - student_netids = form["netids"].replace(" ", "").lower().split(",") + student_netids = request.form["netids"].replace(" ", "").lower().split(",") for student_netid in student_netids: if not util.valid_id(student_netid) or not verify_student(student_netid, cid): return util.error(f"Invalid or non-existent student NetID: {student_netid}") try: - max_runs = int(form["max_runs"]) + max_runs = int(request.form["max_runs"]) if max_runs < 1: return util.error("Max Runs must be a positive integer.") except ValueError: return util.error("Max Runs must be a positive integer.") - start = util.parse_form_datetime(form["start"]).timestamp() - end = util.parse_form_datetime(form["end"]).timestamp() + start = util.parse_form_datetime(request.form["start"]).timestamp() + end = util.parse_form_datetime(request.form["end"]).timestamp() if start >= end: return util.error("Start must be before End.") @@ -303,13 +278,11 @@ def staff_add_extension(netid, cid, aid): db.add_extension(cid, aid, student_netid, max_runs, start, end) return util.success("") - @blueprint.route("/staff/course///extensions/", methods=["DELETE"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_delete_extension(netid, cid, aid): - form = request.form - extension_id = form["_id"] + extension_id = request.form["_id"] delete_result = db.delete_extension(extension_id) if delete_result is None: @@ -327,27 +300,26 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): return abort(HTTPStatus.NOT_FOUND) # form validation - missing = util.check_missing_fields(form, "run_time", "due_time", "name", "config") + missing = util.check_missing_fields(request.form, "run_time", "due_time", "name", "config") if missing: return util.error(f"Missing fields ({', '.join(missing)}).") - run_time = util.parse_form_datetime(form["run_time"]).timestamp() + run_time = util.parse_form_datetime(request.form["run_time"]).timestamp() if run_time is None: return util.error("Missing or invalid run time.") if run_time <= util.now_timestamp(): return util.error("Run time must be in the future.") - due_time = util.parse_form_datetime(form["due_time"]).timestamp() + due_time = util.parse_form_datetime(request.form["due_time"]).timestamp() if due_time is None: return util.error("Missing or invalid due time.") - if "roster" not in form or not form["roster"]: + if "roster" not in request.form or not request.form["roster"]: roster = None else: - roster = form["roster"].replace(" ", "").lower().split(",") + roster = request.form["roster"].replace(" ", "").lower().split(",") for student_netid in roster: if not util.valid_id(student_netid) or not verify_student(student_netid, cid): return util.error(f"Invalid or non-existent student NetID: {student_netid}") try: - config = json.loads(form - ["config"]) + config = json.loads(request.form["config"]) msg = bw_api.set_assignment_config(cid, f"{aid}_{run_id}", config) if msg: return util.error(f"Failed to upload config to Broadway: {msg}") @@ -366,37 +338,32 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): assert scheduled_run_id is not None - if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, form["name"], scheduled_run_id): + if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, request.form["name"], scheduled_run_id): return util.error("Failed to save the changes, please try again.") return util.success("") - @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_schedule_run(netid, cid, aid): - form = request.form # generate new id for this scheduled run run_id = db.generate_new_id() - return add_or_edit_scheduled_run(cid, aid, run_id, form, None) + return add_or_edit_scheduled_run(cid, aid, run_id, request.form, None) - @blueprint.route("/staff/course///schedule_run/", methods=["POST"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_edit_scheduled_run(netid, cid, aid, run_id): - form = request.form sched_run = db.get_scheduled_run(cid, aid, run_id) if sched_run is None: return util.error("Could not find this scheduled run. Please refresh and try again.") if sched_run["status"] != sched_api.ScheduledRunStatus.SCHEDULED: return util.error("Cannot edit past runs") scheduled_run_id = sched_run["scheduled_run_id"] - return add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id) + return add_or_edit_scheduled_run(cid, aid, run_id, request.form, scheduled_run_id) - @blueprint.route("/staff/course///schedule_run/", methods=["GET"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_get_scheduled_run(netid, cid, aid, run_id): sched_run = db.get_scheduled_run(cid, aid, run_id) @@ -405,9 +372,8 @@ def staff_get_scheduled_run(netid, cid, aid, run_id): del sched_run["_id"] return util.success(json.dumps(sched_run), 200) - @blueprint.route("/staff/course///schedule_run/", methods=["DELETE"]) - @auth.require_auth_or_course_token + @auth.require_auth @auth.require_admin_status def staff_delete_scheduled_run(netid, cid, aid, run_id): sched_run = db.get_scheduled_run(cid, aid, run_id) @@ -417,3 +383,6 @@ def staff_delete_scheduled_run(netid, cid, aid, run_id): if not db.delete_scheduled_run(cid, aid, run_id): return util.error("Failed to delete scheduled run. Please try again") return util.success("") + + + \ No newline at end of file From 3d6cb2dc7f7cfd7bcde3d085657a8020cfe57561 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sat, 17 Feb 2024 16:43:37 -0600 Subject: [PATCH 11/23] Fixed issues with routes API parsing JSON --- src/__pycache__/__init__.cpython-312.pyc | Bin 5555 -> 0 bytes src/__pycache__/__init__.cpython-38.pyc | Bin 3561 -> 0 bytes src/__pycache__/auth.cpython-37.pyc | Bin 5707 -> 5888 bytes src/__pycache__/bw_api.cpython-37.pyc | Bin 4804 -> 4810 bytes src/__pycache__/db.cpython-37.pyc | Bin 10791 -> 10797 bytes src/__pycache__/ghe_api.cpython-37.pyc | Bin 1720 -> 1720 bytes src/__pycache__/routes_admin.cpython-37.pyc | Bin 14039 -> 14039 bytes src/__pycache__/routes_api.cpython-37.pyc | Bin 2390 -> 7846 bytes src/__pycache__/routes_student.cpython-37.pyc | Bin 4330 -> 4330 bytes src/__pycache__/sched_api.cpython-37.pyc | Bin 2081 -> 2087 bytes .../template_filters.cpython-37.pyc | Bin 1813 -> 1819 bytes src/__pycache__/util.cpython-37.pyc | Bin 6105 -> 6105 bytes src/auth.py | 12 +++- src/routes_api.py | 60 ++++++++++++++---- src/util.py | 1 - 15 files changed, 56 insertions(+), 17 deletions(-) delete mode 100644 src/__pycache__/__init__.cpython-312.pyc delete mode 100644 src/__pycache__/__init__.cpython-38.pyc diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 94e84b2dacb63c3b1347eb7c269d9eee47672608..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5555 zcmb^!U2hx5aqswf{L}|UNtR?$mL;39EZVgLCy3$Hj^&S_ksV8E5w_@o;vJ=v?s)Xx z(Y6Gspwpn1k)l>m10j$W&_hu;unV|-%75q!8L5?gkdYY8OW$NTPeva)v&SPT#X^gs z3vzFE=VNDgW@l#q8V&~tw8DwM$bSzJ@(&z1%~Jzj{L4kiXG9_rrxJ}za~hxKHCNiD zxzldVllExdv{&<`eVRY*=a7z91uc*cFxaIAwNN_5V7D69BIyW&J!({INw+ZAtG51$ zXOK^A)7sPRS}YyYI?^3%&acL`&U7b(1+`1-PIognpzhOp(mf0gs=ZoYx{tvjb-&i1 z?q_gVO=tt@0gmuQ=HvqpIeSm(LHVFOID5d^YU3+KPQw7GTjZffQHOpH+7_ghhxfp? zGVGx}ux$)G0NAUI)g5T|ngcbyQhQCa_SwnCtOLn|j;?I4fn8%>ipdA$Bab>Bf-SM7 zM_`YA^2iRZ?p@=~UE`kSu{<*SQ(IPZFU6(KY3@GtRu8ERD<*cdGc`Hg`Ql_FQYXvo9%9fAH*Nd`Y zT7D|qWR@p0rBgG>S-uI`Fcduxpny^f8EVKd2wa++ygFfK%%TCK;Cp(0T7O4XIxZC(NP*;_4I8$(p_w z6xEa|^0!aRd6~i%#H>McqN&fxc`GzAHZgJe%D6cB;ngu9a?j|nwWu@u-j(r-SI(ca z+ICHdFefmi_`$WyR-hqqLD2I@C0WblB@h>2r$FVLa%;htQuS#ipR!yR$0h;i(`0i- zmyB$)G4amf{eJNpbe|Ek7VQX`t=-0EV#jYL$QRsrlBcbJw1QJItRk|tz927<{^jx1 zjIPNk#kgL~fYnk{RL@8^GxJC5x~2@8O^KqCS4>eHEzDa1CIBorEz`pwqaXf8J9H)T zotK1KHiA9XV9#2xuj1~b!!W-PukFuf%w|+|DwCZ{{Xe!x2VntcgAs6$f+~X@*-CT> za7+Vw2p}u6bA5UQCXvLz(BCCZ@N}<#(XQ*9qYh#AL}FF|&}&cemN-?ipCi zNMMAqw+$U>`#KFwAIdhG#4d2#rsM>e zds>zf3Q!n{OnyF5G-Ns&5G>c&_>Zk%jb4M`V7Ut!2ry*9GfH*cthxBOY+jamT=+9wR4xxpC22iXgCN`blR|7)3Qm?K$bTzo2s6rXlct0VN|oP zq0f*_v8YH?00g-1F~oa-Sdk38%87*Su72BHGX}-l0B^nV5p;0N0>tCL^X~0;my1<* zN4f7S_wbew`Srp^bg&v7ToVpf_=8*i=)KV0(1yRO>hCJY*Zhf6?1?{gUnn2^ZFHk$ zxY{!O;J{ZcBU?htO6Qu;T{;NRz23XM8$x$g=q`_}35QA@Tb$=k@9o}Yd7X=G`CC?E zpF}EL+`bupycrOIQ`veX0`30CD7E7ndu_pAaP%$QG>W&6gq{JAb#EY|1v(aUgz&xK z-QXwT3fE!l7D*Y9Qj~1!G}`sd4h`MF#V#%SUvnIlIB>LDECom7$1SfdCCO2|qwqmf zHR)poZ2~c;s4_hVNL0|ks|ex5-&4M{Dy{jCRJbEdt(V-+vqHyUnydoQtjVjwN8=JK zaZRxwceATCQSyr%jW6;>%=9@#CVCxfQvI?ymkxqVu#V*y$sI?K;sNY2dJ*;SyZO47w_OUD4F7oaonAQF!2yYUv% zi!k*L;?6_I3?1#*i1b$@{i~j}$Z%=jR;caXmAhBUV{4%UrLJf0$fviqgx33q%Bi)s z*Vcq175>PUzq1^C@cO#{c!fL8tdIp^^@hjCOz;Z;{*wuuq>9n>7DwR$(}?1;^(bDK z4OGs-E^(4e;y&h<_{Q-sa*O;Ue#Zf4Q5C15K+LKywxNR;8n@(9$dVfZxwyzf8iCYH z&o+5N&2t@g{MHrrEP9$#&MTGmEJDJ-=89@MDi@UJ}19-=QxEdy8poa zhMOSZVG+r@Sa{o6{lfi{w1%zF3UV_X{q3)9* zz|Y8utWL2MO+Xb|(4kySK!|0Cn)6`{$mKwaaZ@p~Gcrv~Wems%kTDp05!mylI-f9P z732UWs~4dvO=X}`oz7!k0A{TFG}-bnVU|l&q?g>N#feq~0=?KEwZaV;h?2bJ$|@4% z4(I|%Y73|D!wDxnb|w&To&|pU=7es@RLRZ5?ZF#^;UVe8#DiK{frfm`t?3fzM6m$2 z{8&k1?yR{6~8fh8j@8;HO`EFk6{8t z8h?bYM79GY*jgIe3`a|cpLv4YUg8TbPk*Fug<9^7-yL5I^_Iq-gj(xumYwM9V%V=7Sh)7mC2P0<$>~8`BbHKuqqr|y#-x`KgO;( zB-RU{F~w&rmee(;s2x4US2jCzMrFArC2KlCsm`}ED8*iNYV_wIXdM0q<|>H#kc;-; z-H#cyV>8tHRcPdX?lb*UeN|qIkCeJzxV!k{-<>2vcr)0(85{b};}Trk#KpUq(&NcD zEl&=gVH86F#qeYYO|&YDoUGQcqbZgmR+y2ChW)&v7{9IXO_|Ou$i?YVa1Fz*M%*)| zS+ENiUtu1eQPVn&pwN@Z;esNcVm@V#X?tS}%t1MXZ!9r`?=6bq$ZDRp9W{p2k!I?G zLa}zV+L|V72B26pS}{a84@XR&_!6U?i1H(vOQMSNVc zmkfImuxiT+Ie3O-bqD5mmXoYjs0B^=mMIpfo`r*lK!k2!wNZFr8L$;z8F)$tpOT)Zr2ktI`Lu4^U$W%76Sy5%K2s(8o+OSu?q5%wwWlh<-YV(a9DLmw z_Jgd=eSQ#9NJo`)tf*BoxJi5^{*LdqZ`t*6=m}w;XQ7Ujxz#tm z^sk4`mV_rHysWH~t|uh8e0iP38((2LxlUr=aDhAhxBFMRS3g+2Sn6NnMm9Om2G>#L zI-c_!xBrEU@Nw*pZo6T)?I(N?87i&G$77FAA;()tIDj;#w-Kt-tn{qjSe-$V)N?+_ zbt1{JZG^V>)x@mqTa{OTf+R=Tx@}di4aKn~+mT@u*NxX@R$2!hj6GoMok6RT5vB RUnc|4NOZ;ZHxgq$@^4ZW+1mgB diff --git a/src/__pycache__/__init__.cpython-38.pyc b/src/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 8db5abe0b193a411a8a734cdd1f6afd433311d41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3561 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 delta 952 zcmX|;&1(}u7{>SA-DEdkn{ArSN0Zp}BdKZAXh5vCTF?|NB30}`90aM%VokHQPF4hs zSk!|E3S~qDJt%k(1U=*+UIdR)5JdzL{1+52BA$HT6x?Ng&oj?V-kEo^UfZl%2V=3Y zg3s5{Pwqu$!^*(#jjyalYngR%@o zc^Iz1LiZh<0!Vjr+82Pb{H424?4At#0N5oD8B0*>J~wUv9F!BGRoE{dg?_+-yb~T8 z?&w@uG27Ez?dU5(G1o!mYF1g%#GD`SgH1zz59h&@f5Qd2Ez+=$;fHKT>2sO1e8V@Jp_{O%+*UeR^&YwkEL6gxjHie+!BphSm{}KN zG>H)C+QJ}cgb_qV7di5ILXzOfs*$yq(6%jvqDCPMmON!G#~FpaHjQLRROLtO8|;uT z;zyw-|HPM|E}30|mRz^fFfAY2uQ55^H)2lW?S8DAl}N3%dhYy%GiyudTr9lxNV5=w+|LX|L0Xb^gR&B|w~vO(=cAu2zoMnkrl JFq8P2{{RkV!k_>E diff --git a/src/__pycache__/bw_api.cpython-37.pyc b/src/__pycache__/bw_api.cpython-37.pyc index 5c8cfcc6f8c7b444c95208974223fa091fd4ed0e..90a5ec843eb2bb44f75bbca90c8a99e812df1ec5 100644 GIT binary patch delta 34 pcmX@2dP;S}H%3uY{fzwFRQ=52!qUW|)D->XVq+7-&8$rO1pw+T3{?OC delta 28 jcmX@5dPH@@H%5Ma{fzwFRQ=52!qUW|)RfKKO#1}@q$vur diff --git a/src/__pycache__/db.cpython-37.pyc b/src/__pycache__/db.cpython-37.pyc index ee6cfcc8e1d48bd2a8fcc514ff00b905b9889d27..7119d885b58002d54f66f7674c41f55174670f41 100644 GIT binary patch delta 34 pcmZ1;vNmMHIwny={fzwFRQ=52!qUW|)D->XVq+7-&3l=$)B)&B45$DA delta 28 jcmZ1*vOHwNIwpQ?{fzwFRQ=52!qUW|)RfJKnX=RYqcRHx diff --git a/src/__pycache__/ghe_api.cpython-37.pyc b/src/__pycache__/ghe_api.cpython-37.pyc index 972035bb3ad5a00c018cff0d1a70f7d83edebb2d..ac083e37ac8577819e065f0f132d1c66e01a9c1f 100644 GIT binary patch delta 20 acmdnNyMve8iIx$ delta 19 Zcmcbfdp(!SiI8!=Labf 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 RW|0uLr$5J7RNUS&{~ss%P%!`i delta 539 zcmY*WPiqrF6yLWyyED5<_7I5;rj0oWcA-{!5{oTp6YD`lTvXW0l4PcKVK*tWGk7UQ zs5ehxzJo_EJ$m#D=tW_!o&-OHGkb_S@E&jY{h9aPd_4HxjURP7kz)?K^^NUBtGExZ zR=4l$Rye~I$w(DuAvn~bei>D*tOd@oLj#K6I}~5KS(`%YBxLnzbQfut&NPyuC^ec& zajJva+kARfB%r(L{FG5yGWtR;QZW^>Ru5quBlECt;>bk(SSgqU)G>ykx^REGzd+5s z*Wjzq-T;vL;XQ|;+D0ivSLf)13vEU4!~oCm{T=JVuiHdS(qVe^WHNsFbTZanEx3-} zFps zW-a8FZ|ACATR?M9I(%FG-Wv40n$Nl2ByW}w*b1w?;kA7$X}JFkv^Le`LAA7hatMaj MbVHyGZMSdY51(gu7XSbN diff --git a/src/__pycache__/routes_student.cpython-37.pyc b/src/__pycache__/routes_student.cpython-37.pyc index 3fa32036b749f231f003a6d38231570125573e28..b391f84cf7629a8da472d1f0ca5073e541120ba0 100644 GIT binary patch delta 20 acmaE*_)3x6iIp;enx(7s(xm1VQFGfYRcv=#<^?&j?)Sj diff --git a/src/__pycache__/template_filters.cpython-37.pyc b/src/__pycache__/template_filters.cpython-37.pyc index 3c5e843c5624d4457f33508e5796ab200428ff7f..3d846789f1d73dc47aed2058717d2f5fa718bfa8 100644 GIT binary patch delta 34 pcmbQrH=A$6Zbngi{fzwFRQ=52!qUW|)D->XVq+7-&8HZ>Spn3-3*7(! delta 28 jcmbQuHk4)-R!!a} zc$cwu^Aw@?jEr5ATSST(TQ>g?5oKhYF_}*+opJK!GO-XweijaP4i*kJAZ7-#_yMjV B9h3k7 diff --git a/src/auth.py b/src/auth.py index fda5825..09f822f 100644 --- a/src/auth.py +++ b/src/auth.py @@ -1,5 +1,6 @@ from functools import wraps from http import HTTPStatus +import json from flask import session, redirect, url_for, abort, request, render_template, make_response from src.common import verify_staff, verify_admin @@ -81,6 +82,7 @@ def wrapper(*arg, **kwargs): token = request.headers.get("Authorization", None) cid = kwargs[CID_KEY] course = get_course(cid) + print(course["token"]==token) if course is None or ("token" in course and token != course["token"]): return abort(HTTPStatus.FORBIDDEN) return func(*arg, **kwargs) @@ -97,8 +99,14 @@ def require_admin_status(func): @wraps(func) def wrapper(*args, **kwargs): netid = kwargs.get(UID_KEY, None) - if request.json is not None: - netid = request.json.get(UID_KEY, netid) + if netid is None and request.json is not None: + _json = request.json + if isinstance(_json, str): + try: + _json = json.loads(request.json) + except json.JSONDecodeError: + return abort(HTTPStatus.BAD_REQUEST) + netid = _json.get(UID_KEY, netid) cid = kwargs[CID_KEY] if netid is None or not verify_staff(netid, cid) or not verify_admin(netid, cid): return abort(HTTPStatus.FORBIDDEN) diff --git a/src/routes_api.py b/src/routes_api.py index ef5deee..6276f2d 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -1,6 +1,7 @@ import logging from flask import request from http import HTTPStatus +from functools import wraps import json from src import db, util, auth, bw_api @@ -9,6 +10,7 @@ MIN_PREDEADLINE_RUNS = 1 # Minimum pre-deadline runs for every assignment + class ApiRoutes: def __init__(self, blueprint): @blueprint.route("/api//update_roster", methods=["POST"]) @@ -62,15 +64,22 @@ def trigger_scheduled_run(cid, aid, scheduled_run_id): @blueprint.route("/api//add_extensions", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status - def add_extensions(netid, cid, aid): + def add_extensions(cid, aid): + form = request.json + if isinstance(form, str): + try: + form = json.loads(request.json) + except json.JSONDecodeError: + return util.error("Failed to decode config JSON") + assignment = db.get_assignment(cid, aid) if not assignment: return util.error("Invalid course or assignment. Please try again.") - if util.check_missing_fields(request.json, "extensions"): + if util.check_missing_fields(form, "extensions"): return util.error("Missing fields. Please try again.") - for ext_json in request.json: + for ext_json in form: if util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end"): return util.error("Missing fields. Please try again.") @@ -98,8 +107,13 @@ def add_extensions(netid, cid, aid): @blueprint.route("/api//add_assignment", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status - def api_add_assignment(netid, cid): + def api_add_assignment(cid): form = request.json + if isinstance(form, str): + try: + form = json.loads(request.json) + except json.JSONDecodeError: + return util.error("Failed to decode config JSON") missing = util.check_missing_fields(form, *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) if missing: @@ -124,15 +138,19 @@ def api_add_assignment(netid, cid): if not db.Quota.is_valid(quota): return util.error("Quota Type is invalid.") - start = util.parse_form_datetime(form["start"]).timestamp() - end = util.parse_form_datetime(form["end"]).timestamp() + start = util.parse_form_datetime(form["start"]) + end = util.parse_form_datetime(form["end"]) if start is None or end is None: return util.error("Missing or invalid Start or End.") + start = start.timestamp() + end = end.timestamp() if start >= end: return util.error("Start must be before End.") try: - config = json.loads(form["config"]) + config = form["config"] + if not isinstance(config, dict): + config = json.loads(config) msg = bw_api.set_assignment_config(cid, aid, config) if msg: @@ -172,7 +190,9 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): if not util.valid_id(student_netid) or not verify_student(student_netid, cid): return util.error(f"Invalid or non-existent student NetID: {student_netid}") try: - config = json.loads(form["config"]) + config = form["config"] + if not isinstance(config, dict): + config = json.loads(config) msg = bw_api.set_assignment_config(cid, f"{aid}_{run_id}", config) if msg: return util.error(f"Failed to upload config to Broadway: {msg}") @@ -198,23 +218,35 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): @blueprint.route("/api///schedule_run/", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status - def api_add_scheduled_run(netid, cid, aid): + def api_add_scheduled_run(cid, aid): # generate new id for this scheduled run + form = request.json + if isinstance(form, str): + try: + form = json.loads(request.json) + except json.JSONDecodeError: + return util.error("Failed to decode config JSON") run_id = db.generate_new_id() - return add_or_edit_scheduled_run(cid, aid, run_id, request.json, None) + return add_or_edit_scheduled_run(cid, aid, run_id, form, None) @blueprint.route("/api///schedule_runs/", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status - def api_add_scheduled_runs(netid, cid, aid): + def api_add_scheduled_runs(cid, aid): + form = request.json + if isinstance(form, str): + try: + form = json.loads(request.json) + except json.JSONDecodeError: + return util.error("Failed to decode config JSON") # generate new id for this scheduled run - missing = util.check_missing_fields(request.json, "runs") + missing = util.check_missing_fields(form, "runs") if missing: return util.error(f"Missing fields {', '.join(missing)}") # TODO: there's probably a better way to do this - if isinstance(request.json["runs"], list): + if isinstance(form["runs"], list): return util.error("runs field must be a list of run configs!") - for run_config in request.json["runs"]: + for run_config in form["runs"]: run_id = db.generate_new_id() retval = add_or_edit_scheduled_run(cid, aid, run_id, run_config, None) # TODO: There should be a distinction between good and bad responses diff --git a/src/util.py b/src/util.py index db3090e..92d4074 100644 --- a/src/util.py +++ b/src/util.py @@ -97,7 +97,6 @@ def verify_csrf_token(client_token): def valid_id(id_str): return bool(fullmatch(r'[a-zA-Z0-9_.\-]+', id_str)) - def parse_form_datetime(datetime_local_str): try: return TZ.localize(datetime.strptime(datetime_local_str, "%Y-%m-%dT%H:%M")) From fb747966427524edc8bed88936c2d53ee2af72b2 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sun, 18 Feb 2024 15:22:25 -0600 Subject: [PATCH 12/23] Edited return codes and fixed functionality with APIs --- src/__pycache__/bw_api.cpython-37.pyc | Bin 4810 -> 4810 bytes src/__pycache__/routes_admin.cpython-37.pyc | Bin 14039 -> 14076 bytes src/__pycache__/routes_api.cpython-37.pyc | Bin 7846 -> 8240 bytes src/__pycache__/routes_staff.cpython-37.pyc | Bin 5047 -> 5047 bytes src/routes_admin.py | 3 +- src/routes_api.py | 53 ++++++++++++-------- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/__pycache__/bw_api.cpython-37.pyc b/src/__pycache__/bw_api.cpython-37.pyc index 90a5ec843eb2bb44f75bbca90c8a99e812df1ec5..11d78b4f7ad682e8e7f6a0244dd13ce1f9b795c9 100644 GIT binary patch delta 20 acmX@5dPwV_$k<&LkHna~n-e`lWohTRZN zdrLB;Hn3Z|&~a3JhO{nh*i4udE(n_95Oi7HuLkC+qJv+82Lr)>`*s;l!)3S)kLdz| z?+jUcLTdnFd=`G*C)JuYt*Kwu%2hVNJH%6wO=*r#?(?@$kxfI=m?j2!YTya zg;j|eVisqVF-MJ0v3Co9OU(vp<*V2%&s8gz*j0@! z_bLnNI9x$B{U+An(asj-F6QC`TP55?n#d7lJe`R;=`^;6jm#iuxRKfBA6dv2U=^+G ziQZyY^r2@Y0*e?Q*&3x^!Zc!mxJDSn9imMZ5jHwV5Y!Nhf(Oc}fP90D1o4nVzyEA9iZMt1|=&rUM1U~6ABK{_Tj4OdR zSP3qRd7k4DKdYxP5jtWNLJmt{ia)a~K{1&^t=7`K#3+P8@or`Cms^Fh(GaoGB2J?_#tLY zBqHEg;7f5}kMTh=0+)@~$qc|EZuRA5TG@0~vokqmlGVy=@o#576@zQoNUe@kc27@q<=LXk?>6bB;m?dh&ZQ?G`AezK;;sw#oiJSOqj;3>v#O1VRYjx__74qPz Mp<^(GcZTY}0ZPxT=l}o! diff --git a/src/__pycache__/routes_api.cpython-37.pyc b/src/__pycache__/routes_api.cpython-37.pyc index 99ab7b031e480c644dbe04228b748042827d5938..bdde2fa389255fd7f4b13a3a6ed230b375c9b480 100644 GIT binary patch delta 2242 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 diff --git a/src/__pycache__/routes_staff.cpython-37.pyc b/src/__pycache__/routes_staff.cpython-37.pyc index 058eacad0168f651e1dcbdfb65a6207c84dbbe0c..a940d938ecc4bc80d75458f7d423a75f279ba462 100644 GIT binary patch delta 20 acmdn4zFnQ$iI/add_extensions", methods=["POST"]) + @blueprint.route("/api///add_extensions", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status def add_extensions(cid, aid): @@ -77,11 +77,12 @@ def add_extensions(cid, aid): return util.error("Invalid course or assignment. Please try again.") if util.check_missing_fields(form, "extensions"): - return util.error("Missing fields. Please try again.") + return util.error(f"Missing fields extensions.\nPlease try again.") - for ext_json in form: - if util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end"): - return util.error("Missing fields. Please try again.") + for ext_json in form["extensions"]: + missing = util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end") + if missing: + return util.error(f"Extension missing fields {', '.join(missing)}. Please try again.") student_netids = ext_json["netids"].replace(" ", "").lower().split(",") for student_netid in student_netids: @@ -95,14 +96,22 @@ def add_extensions(cid, aid): except ValueError: return util.error("Max Runs must be a positive integer.") - start = util.parse_form_datetime(ext_json["start"]).timestamp() - end = util.parse_form_datetime(ext_json["end"]).timestamp() + print(ext_json["start"], ext_json["end"]) + + start = util.parse_form_datetime(ext_json["start"]) + if not start: + return util.error("Failed to parse timestamp") + start = start.timestamp() + end = util.parse_form_datetime(ext_json["end"]) + if not end: + return util.error("Failed to parse timestamp") + end = end.timestamp() if start >= end: return util.error("Start must be before End.") for student_netid in student_netids: db.add_extension(cid, aid, student_netid, max_runs, start, end) - return util.success("") + return util.success("Successfully uploaded extensions", HTTPStatus.OK) @blueprint.route("/api//add_assignment", methods=["POST"]) @auth.require_course_auth @@ -124,7 +133,7 @@ def api_add_assignment(cid): return util.error("Invalid Assignment ID. Allowed characters: a-z A-Z _ - .") new_assignment = db.get_assignment(cid, aid) - if new_assignment: + if new_assignment and not request.args.get('overwrite', False): return util.error("Assignment ID already exists.") try: @@ -161,7 +170,9 @@ def api_add_assignment(cid): visibility = form["visibility"] db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) - return util.success("") + msg = "Successfully added assignment." if not new_assignment else \ + "Successfully updated assignment." + return util.success(msg, HTTPStatus.OK) def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): # course and assignment name validation @@ -202,20 +213,20 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): # Schedule a new run with scheduler if scheduled_run_id is None: scheduled_run_id = schedule_run(run_time, cid, aid) - if scheduled_run_id is None: - return util.error("Failed to schedule run with scheduler") + # if scheduled_run_id is None: + # return util.error("Failed to schedule run with scheduler") # Or if the run was already scheduled, update the time else: if not update_scheduled_run(scheduled_run_id, run_time): return util.error("Failed to update scheduled run time with scheduler") - assert scheduled_run_id is not None + # assert scheduled_run_id is not None if not db.add_or_update_scheduled_run(run_id, cid, aid, run_time, due_time, roster, form["name"], scheduled_run_id): return util.error("Failed to save the changes, please try again.") - return util.success("") + return util.success("Successfully scheduled run.", HTTPStatus.OK) - @blueprint.route("/api///schedule_run/", methods=["POST"]) + @blueprint.route("/api///schedule_run", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status def api_add_scheduled_run(cid, aid): @@ -229,7 +240,7 @@ def api_add_scheduled_run(cid, aid): run_id = db.generate_new_id() return add_or_edit_scheduled_run(cid, aid, run_id, form, None) - @blueprint.route("/api///schedule_runs/", methods=["POST"]) + @blueprint.route("/api///schedule_runs", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status def api_add_scheduled_runs(cid, aid): @@ -244,12 +255,14 @@ def api_add_scheduled_runs(cid, aid): if missing: return util.error(f"Missing fields {', '.join(missing)}") # TODO: there's probably a better way to do this - if isinstance(form["runs"], list): + print(form["runs"]) + print(type(form["runs"])) + if not isinstance(form["runs"], list): return util.error("runs field must be a list of run configs!") for run_config in form["runs"]: run_id = db.generate_new_id() retval = add_or_edit_scheduled_run(cid, aid, run_id, run_config, None) - # TODO: There should be a distinction between good and bad responses - if retval[1] != HTTPStatus.NO_CONTENT: + # TODO: There should be a better distinction between good and bad responses + if retval[1] != HTTPStatus.OK: return retval - return util.success("") \ No newline at end of file + return util.success("Successfully scheduled runs", HTTPStatus.OK) \ No newline at end of file From 6bff0e65c636df18eeb18e02c8a8d6a570fc0c97 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sun, 18 Feb 2024 15:24:39 -0600 Subject: [PATCH 13/23] Fixed functionality for modifying assignments in add --- src/__pycache__/routes_api.cpython-37.pyc | Bin 8240 -> 8240 bytes src/routes_api.py | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/__pycache__/routes_api.cpython-37.pyc b/src/__pycache__/routes_api.cpython-37.pyc index bdde2fa389255fd7f4b13a3a6ed230b375c9b480..71981f3c2dec447e013caff5f1a82964df4ef759 100644 GIT binary patch delta 19 Zcmdnsu)%@LiId001u21RVeX delta 19 Zcmdnsu)%@LiId001v>1UCQx diff --git a/src/routes_api.py b/src/routes_api.py index f15d9c1..bc580e5 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -169,7 +169,10 @@ def api_add_assignment(cid): visibility = form["visibility"] - db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) + if new_assignment: + db.update_assignment(cid, aid, max_runs, quota, start, end, visibility) + else: + db.add_assignment(cid, aid, max_runs, quota, start, end, visibility) msg = "Successfully added assignment." if not new_assignment else \ "Successfully updated assignment." return util.success(msg, HTTPStatus.OK) From fdd7a8d7ca2d17f299a81c8aab72268e9452c23c Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Sun, 18 Feb 2024 15:26:33 -0600 Subject: [PATCH 14/23] Uncommented sched_api (sched_api is untested, but *should* work) --- src/__pycache__/routes_api.cpython-37.pyc | Bin 8240 -> 8356 bytes src/routes_api.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__pycache__/routes_api.cpython-37.pyc b/src/__pycache__/routes_api.cpython-37.pyc index 71981f3c2dec447e013caff5f1a82964df4ef759..4ab12114150adf1b90cefe4743020f9a6bf4e8a8 100644 GIT binary patch delta 457 zcmdnsu*8wqiIcSUdYbKkS7Kdm&Oza=_xiy5zk{vk*MV=VOta#-zy~grk|+7$qiqiTE<{F>RhH zvXIgAP>CeadWIUtX69O!8kQ7>Y>o+xMK?;A7qFxN<%2->z*r0s7o8j} zCNIlW%m7v;22urN$)t#vux81E9VM}Ox|k`WnyOo3W=?8~LP@?tadJj#N@-52LQ!d+ zLV0FM23(|Q^J(!sMyU*7XccLJ2tg1L0wTgeMB3zFNnN%rKxVPu4l}t}BrQ8p%+@*vwqZk|$OoRl`!l(abb~vG_;{ z^8%I>pqgNY5(c30OdwT2wVX9vHOwh2l0YXc!H@Z3Hrly(WK?bY|jX-E1%A$jFfc6w+iY z%AMRHt-| Date: Tue, 20 Feb 2024 13:00:26 -0600 Subject: [PATCH 15/23] Somehow this got deleted? --- .gitignore | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67be4a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +config.py + +.idea/ + +.vscode/ \ No newline at end of file From 346084501d764fb7a79ce0e88704806620da1fa6 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Tue, 20 Feb 2024 13:01:06 -0600 Subject: [PATCH 16/23] Somehow this also got deleted? --- requirements.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..90903b2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +certifi==2018.11.29 +chardet==3.0.4 +Click==7.0 +Flask==1.0.2 +Flask-PyMongo==2.2.0 +Flask-Session==0.3.1 +humanize==0.5.1 +idna==2.8 +itsdangerous==1.1.0 +Jinja2==2.11.3 +MarkupSafe==1.1.0 +pymongo==3.7.2 +pytz==2018.9 +requests==2.21.0 +urllib3==1.24.2 +werkzeug==0.15.5 +ansi2html==1.5.2 +gunicorn==20.0.4 +identity==0.3.2 \ No newline at end of file From 82c1b0f64a00e8942c8a79ea0f78d392aaffadc7 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Tue, 20 Feb 2024 13:03:43 -0600 Subject: [PATCH 17/23] Removed unnecessary str check for json decoding --- src/__pycache__/__init__.cpython-37.pyc | Bin 3507 -> 0 bytes src/__pycache__/auth.cpython-37.pyc | Bin 5888 -> 0 bytes src/__pycache__/bw_api.cpython-37.pyc | Bin 4810 -> 0 bytes src/__pycache__/common.cpython-37.pyc | Bin 3631 -> 0 bytes src/__pycache__/db.cpython-37.pyc | Bin 10797 -> 0 bytes src/__pycache__/ghe_api.cpython-37.pyc | Bin 1720 -> 0 bytes src/__pycache__/routes_admin.cpython-37.pyc | Bin 14076 -> 0 bytes src/__pycache__/routes_api.cpython-37.pyc | Bin 8356 -> 0 bytes src/__pycache__/routes_staff.cpython-37.pyc | Bin 5047 -> 0 bytes src/__pycache__/routes_student.cpython-37.pyc | Bin 4330 -> 0 bytes src/__pycache__/sched_api.cpython-37.pyc | Bin 2087 -> 0 bytes .../template_filters.cpython-37.pyc | Bin 1819 -> 0 bytes src/__pycache__/util.cpython-37.pyc | Bin 6105 -> 0 bytes src/routes_api.py | 20 ------------------ 14 files changed, 20 deletions(-) delete mode 100644 src/__pycache__/__init__.cpython-37.pyc delete mode 100644 src/__pycache__/auth.cpython-37.pyc delete mode 100644 src/__pycache__/bw_api.cpython-37.pyc delete mode 100644 src/__pycache__/common.cpython-37.pyc delete mode 100644 src/__pycache__/db.cpython-37.pyc delete mode 100644 src/__pycache__/ghe_api.cpython-37.pyc delete mode 100644 src/__pycache__/routes_admin.cpython-37.pyc delete mode 100644 src/__pycache__/routes_api.cpython-37.pyc delete mode 100644 src/__pycache__/routes_staff.cpython-37.pyc delete mode 100644 src/__pycache__/routes_student.cpython-37.pyc delete mode 100644 src/__pycache__/sched_api.cpython-37.pyc delete mode 100644 src/__pycache__/template_filters.cpython-37.pyc delete mode 100644 src/__pycache__/util.cpython-37.pyc diff --git a/src/__pycache__/__init__.cpython-37.pyc b/src/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 0559eb88fdd15fd0778efe40f390d066ed73c7ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3507 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 diff --git a/src/__pycache__/common.cpython-37.pyc b/src/__pycache__/common.cpython-37.pyc deleted file mode 100644 index 2db4e0fbc1cb20e1ef4031f64f0cbcc2c9e6dc0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3631 zcmcgvO>Y}T7~a`0uN^y0-R}=zg$l)mCRDTtl@eN-OQp&PwHJJ-wehUeZT2Ib9jA@8 zIi(Wi07sAzLJc?m1;1mioH%pi#PiO^cAT0jy>zXad3WBKoq3-3W5!=K8Z`sYPmMpr zBXfrF7rl%g3&Ks5{6|#W;4C&0qhm0V&6p)-$JDYFTZ!GVwQR>u;&$Am(y8G6EO&Y3 z8S8l5U@D8Mcd#_ehlqAKh95}J;G1&Q)n0X zX?_OnQSRI`T5tW0JsB;gJRZnUMo9?S8ps|R^M0pgDwD4&dmy8@-7{${9%>2HZIt|D zR0ZS4x+fN-@q+Cd+pGY{Gw8K3Y@KD7@NS-gbPrRRfn}|>a&j36sjM*NEmOGoifW|U zlP>K6s$>Ie*MdBc*3u+QF%Zf>3{l6xf3Gb)%#v^^${!B`5%Q&8{>jxVORFLa_>*Ak za+Y4^VG^W#DHpw^UX~lwKMHyw%X& zEvoc6VpGlQX=B&cUZ>uf?$jUa_A}AV<$zNFINxYc&i$|N-@25N2f)C!LzMzg|b993j>|(tH`&aZJrmqSCQq z*3BBT&3SWyon%zRC1_@QP8*v4^BnCT@Ez%BOEYf5(zdp=BVAeH7J7`4?F*-H`(!M; zJ=>-}mMXK)bHuLBK4)iSA3>fiDEjxHBM&{5BL*nP2%eGet4)yl15|7Kgw3*@LMrDR0*7}?-M3@g2EdK`WaaO=!v-y34kiGz+$aEP_zPVNpiSDO#tO4MZO1Q-NWxW?76g zgkY)4-iQMfsv45sUL-AHT&`0bTv&Yi+FKg#)T*~1hP`$F$-_`S426%%UxQbqemj(R zR{SXU10RQ2zwakuvWgK|-w&i8hsfUk6~El$Vy$|8BM?F2>j1ruIfFbD7xVHfq!ytZ zh?F#hF$_|FGl&NvR3lb2`_rsoS#cGp_9zk8ARnZ|({x}YDN8b3kntIjL3W{sTB7MT zN>27E#wQ>h@9Qr4@$lzElA)8BAEo=qIy4O>OGs$ML^;d9v+`XQm{?%*pUvc7;kCX0v~leLJK(RFIu zEKJVUtJz1O%+xnU7qXTLxLmd@@99bJdFSKtvlbqq*NHRW%!)3Z-=4VmWaYw4N1+(UQZmn6X LdahT~zq$Va@OWrm diff --git a/src/__pycache__/db.cpython-37.pyc b/src/__pycache__/db.cpython-37.pyc deleted file mode 100644 index 7119d885b58002d54f66f7674c41f55174670f41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10797 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 diff --git a/src/__pycache__/ghe_api.cpython-37.pyc b/src/__pycache__/ghe_api.cpython-37.pyc deleted file mode 100644 index ac083e37ac8577819e065f0f132d1c66e01a9c1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1720 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 diff --git a/src/__pycache__/routes_student.cpython-37.pyc b/src/__pycache__/routes_student.cpython-37.pyc deleted file mode 100644 index b391f84cf7629a8da472d1f0ca5073e541120ba0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4330 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 diff --git a/src/__pycache__/template_filters.cpython-37.pyc b/src/__pycache__/template_filters.cpython-37.pyc deleted file mode 100644 index 3d846789f1d73dc47aed2058717d2f5fa718bfa8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1819 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 diff --git a/src/routes_api.py b/src/routes_api.py index f37a834..28aa639 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -66,11 +66,6 @@ def trigger_scheduled_run(cid, aid, scheduled_run_id): @auth.require_admin_status def add_extensions(cid, aid): form = request.json - if isinstance(form, str): - try: - form = json.loads(request.json) - except json.JSONDecodeError: - return util.error("Failed to decode config JSON") assignment = db.get_assignment(cid, aid) if not assignment: @@ -118,11 +113,6 @@ def add_extensions(cid, aid): @auth.require_admin_status def api_add_assignment(cid): form = request.json - if isinstance(form, str): - try: - form = json.loads(request.json) - except json.JSONDecodeError: - return util.error("Failed to decode config JSON") missing = util.check_missing_fields(form, *["aid", "max_runs", "quota", "start", "end", "config", "visibility"]) if missing: @@ -235,11 +225,6 @@ def add_or_edit_scheduled_run(cid, aid, run_id, form, scheduled_run_id): def api_add_scheduled_run(cid, aid): # generate new id for this scheduled run form = request.json - if isinstance(form, str): - try: - form = json.loads(request.json) - except json.JSONDecodeError: - return util.error("Failed to decode config JSON") run_id = db.generate_new_id() return add_or_edit_scheduled_run(cid, aid, run_id, form, None) @@ -248,11 +233,6 @@ def api_add_scheduled_run(cid, aid): @auth.require_admin_status def api_add_scheduled_runs(cid, aid): form = request.json - if isinstance(form, str): - try: - form = json.loads(request.json) - except json.JSONDecodeError: - return util.error("Failed to decode config JSON") # generate new id for this scheduled run missing = util.check_missing_fields(form, "runs") if missing: From 3e5cd4c0d9e38489e6f6a5abab73b11cb08169e5 Mon Sep 17 00:00:00 2001 From: Andrew Orals Date: Wed, 21 Feb 2024 11:51:26 -0600 Subject: [PATCH 18/23] Make extensions api route atomic --- src/routes_api.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/routes_api.py b/src/routes_api.py index 28aa639..92f0e79 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -69,10 +69,15 @@ def add_extensions(cid, aid): assignment = db.get_assignment(cid, aid) if not assignment: - return util.error("Invalid course or assignment. Please try again.") + return util.error("Invalid course or assignment.\nPlease try again.") if util.check_missing_fields(form, "extensions"): return util.error(f"Missing fields extensions.\nPlease try again.") + + if type(form["extensions"]) != list: + return util.error(f"Field extensions must be a list.\nPlease try again.") + + extensions_to_add = [] for ext_json in form["extensions"]: missing = util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end") @@ -105,7 +110,11 @@ def add_extensions(cid, aid): return util.error("Start must be before End.") for student_netid in student_netids: - db.add_extension(cid, aid, student_netid, max_runs, start, end) + extensions_to_add.append((student_netid, max_runs, start, end)) + + for student_netid, max_runs, start, end in extensions_to_add: + db.add_extension(cid, aid, student_netid, max_runs, start, end) + return util.success("Successfully uploaded extensions", HTTPStatus.OK) @blueprint.route("/api//add_assignment", methods=["POST"]) From 0bf9dd37d3aa6fe8031de8740845ecca78b50268 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Tue, 2 Apr 2024 00:49:23 -0500 Subject: [PATCH 19/23] Tied adding run to adding extension --- src/routes_api.py | 77 ++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/src/routes_api.py b/src/routes_api.py index 28aa639..aca1a2e 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -61,52 +61,61 @@ def trigger_scheduled_run(cid, aid, scheduled_run_id): # Want to avoid stuff like this, with overlaps in function definitions # Best way is to consider an AdminOperations class and have AdminRoutes and APIRoutes # use the functionality defined in there, instead of whatever I did with AdminRoutes currently - @blueprint.route("/api///add_extensions", methods=["POST"]) + @blueprint.route("/api///add_extension", methods=["POST"]) @auth.require_course_auth @auth.require_admin_status - def add_extensions(cid, aid): + def add_extension(cid, aid): form = request.json assignment = db.get_assignment(cid, aid) if not assignment: return util.error("Invalid course or assignment. Please try again.") - if util.check_missing_fields(form, "extensions"): - return util.error(f"Missing fields extensions.\nPlease try again.") - - for ext_json in form["extensions"]: - missing = util.check_missing_fields(ext_json, "netids", "max_runs", "start", "end") - if missing: - return util.error(f"Extension missing fields {', '.join(missing)}. Please try again.") + + missing = util.check_missing_fields(form, "netids", "max_runs", "start", "end") + if missing: + return util.error(f"Extension missing fields {', '.join(missing)}. Please try again.") - student_netids = ext_json["netids"].replace(" ", "").lower().split(",") - for student_netid in student_netids: - if not util.valid_id(student_netid) or not verify_student(student_netid, cid): - return util.error(f"Invalid or non-existent student NetID: {student_netid}") + student_netids = form["netids"].replace(" ", "").lower().split(",") + for student_netid in student_netids: + if not util.valid_id(student_netid) or not verify_student(student_netid, cid): + return util.error(f"Invalid or non-existent student NetID: {student_netid}") - try: - max_runs = int(ext_json["max_runs"]) - if max_runs < 1: - return util.error("Max Runs must be a positive integer.") - except ValueError: + try: + max_runs = int(form["max_runs"]) + if max_runs < 1: return util.error("Max Runs must be a positive integer.") + except ValueError: + return util.error("Max Runs must be a positive integer.") + + print(form["start"], form["end"]) + + start = util.parse_form_datetime(form["start"]) + if not start: + return util.error("Failed to parse timestamp") + start = start.timestamp() + end = util.parse_form_datetime(form["end"]) + if not end: + return util.error("Failed to parse timestamp") + end = end.timestamp() + if start >= end: + return util.error("Start must be before End") + + ext_res = db.add_extension(cid, aid, ','.join(student_netids), max_runs, start, end) + if not ext_res.acknowledged: + return util.error("Failed to add extension to db") + + form = request.json + run_id = db.generate_new_id() - print(ext_json["start"], ext_json["end"]) - - start = util.parse_form_datetime(ext_json["start"]) - if not start: - return util.error("Failed to parse timestamp") - start = start.timestamp() - end = util.parse_form_datetime(ext_json["end"]) - if not end: - return util.error("Failed to parse timestamp") - end = end.timestamp() - if start >= end: - return util.error("Start must be before End.") - - for student_netid in student_netids: - db.add_extension(cid, aid, student_netid, max_runs, start, end) - return util.success("Successfully uploaded extensions", HTTPStatus.OK) + # Add scheduled run if specified in query + if request.args.get("add_run"): + msg, status = add_or_edit_scheduled_run(cid, aid, run_id, form, None) + if status != HTTPStatus.OK: + # Rollback changes to db + db.delete_extension(ext_res.inserted_id) + return util.error(msg) + return util.success("Successfully uploaded extension", HTTPStatus.OK) @blueprint.route("/api//add_assignment", methods=["POST"]) @auth.require_course_auth From 29b32569c391af542680c45e9ded6853cd7754f3 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Tue, 2 Apr 2024 00:49:54 -0500 Subject: [PATCH 20/23] Removed debug print --- src/routes_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes_api.py b/src/routes_api.py index aca1a2e..bcf994a 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -88,7 +88,7 @@ def add_extension(cid, aid): except ValueError: return util.error("Max Runs must be a positive integer.") - print(form["start"], form["end"]) + # print(form["start"], form["end"]) start = util.parse_form_datetime(form["start"]) if not start: From d9ceac35ac6a4f340d0b080b24a6f2f875363125 Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Tue, 2 Apr 2024 01:23:42 -0500 Subject: [PATCH 21/23] Added validation to query param --- src/routes_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes_api.py b/src/routes_api.py index bcf994a..d5a3100 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -109,7 +109,10 @@ def add_extension(cid, aid): run_id = db.generate_new_id() # Add scheduled run if specified in query - if request.args.get("add_run"): + parsed_bool, err = util.parse_bool(request.args.get("add_run")) + if err: + return util.error(err) + if parsed_bool: msg, status = add_or_edit_scheduled_run(cid, aid, run_id, form, None) if status != HTTPStatus.OK: # Rollback changes to db From 0d59a997d00a2d1999b683c65c4dfdd1126c96cd Mon Sep 17 00:00:00 2001 From: ananthm3 Date: Wed, 3 Apr 2024 18:39:23 -0500 Subject: [PATCH 22/23] Fixed errors --- src/routes_api.py | 5 +---- src/util.py | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/routes_api.py b/src/routes_api.py index d5a3100..f74a6ee 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -109,10 +109,7 @@ def add_extension(cid, aid): run_id = db.generate_new_id() # Add scheduled run if specified in query - parsed_bool, err = util.parse_bool(request.args.get("add_run")) - if err: - return util.error(err) - if parsed_bool: + if request.args.get("add_run", False): msg, status = add_or_edit_scheduled_run(cid, aid, run_id, form, None) if status != HTTPStatus.OK: # Rollback changes to db diff --git a/src/util.py b/src/util.py index 92d4074..df52928 100644 --- a/src/util.py +++ b/src/util.py @@ -58,7 +58,6 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - def error(content, status=HTTPStatus.BAD_REQUEST): """ Builds a response pair with the error content and status code. The Bad Request status is used if none is From d81a446c2aba1adef1978a2a8d35be52333e4efd Mon Sep 17 00:00:00 2001 From: Ananth Madan <45344102+ananthm0203@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:19:48 -0500 Subject: [PATCH 23/23] Update routes_api.py Removed duplicate form=request.json --- src/routes_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes_api.py b/src/routes_api.py index 480c917..e3afdea 100644 --- a/src/routes_api.py +++ b/src/routes_api.py @@ -105,7 +105,6 @@ def add_extension(cid, aid): if not ext_res.acknowledged: return util.error("Failed to add extension to db") - form = request.json run_id = db.generate_new_id() # Add scheduled run if specified in query @@ -257,4 +256,4 @@ def api_add_scheduled_runs(cid, aid): # TODO: There should be a better distinction between good and bad responses if retval[1] != HTTPStatus.OK: return retval - return util.success("Successfully scheduled runs", HTTPStatus.OK) \ No newline at end of file + return util.success("Successfully scheduled runs", HTTPStatus.OK)