From b23fdfb6c3f58381beff2fe05e2b258581d1d86a Mon Sep 17 00:00:00 2001 From: aleguy02 Date: Sun, 24 Aug 2025 02:20:44 -0400 Subject: [PATCH 1/6] Handle form validation in frontend and bundle multiple inputs for efficiency --- app/routes/main.py | 33 ++++++++++----------- app/templates/index.html | 64 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/app/routes/main.py b/app/routes/main.py index 9988289..c9c3a4e 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -12,6 +12,7 @@ abort, ) from app.config import get_config +import json main_bp: Blueprint = Blueprint("main", __name__) config = get_config() @@ -48,35 +49,28 @@ def index(): @main_bp.route("/unlocks", methods=["POST"]) def unlocks_redirect(): - raw = request.form.get("tentative-code", "") - code = normalise(raw) - if not code: - return redirect(url_for("main.index")) + code = request.form.get("tentative-code", "") - # extract completed courses - completed = [] - for i in range(1, config.MAX_COURSES_TAKEN + 1): - raw = request.form.get(f"code{i}", "") - if not raw: - continue - - base = normalise(raw) - if not base: - return redirect(url_for("main.index")) - - completed.append(base) + completed = request.form.get(f"completed-courses", "[]") + try: + completed = json.loads(completed) + except json.JSONDecodeError as e: + current_app.logger.exception(f"Error decoding JSON: {e}") + completed = [] sem = request.form.get("semester", current_app.config["DEFAULT_SEMESTER"]) view = request.form.get("view_type", "") - if view != "tcm" and view != "graph": + if view not in ("tcm", "graph"): abort(400, "Bad view type") return redirect( url_for( "main.unlocks_page", code=code, - completed=_to_CSV(completed), + completed=_to_CSV( + completed + ), # optimizaton, pass more efficiently, maybe in a cookie semester=sem, view_type=view, ) @@ -114,6 +108,9 @@ def unlocks_page(code: str): # all courses in unlocks for which you do not meet any other prereqs, excluding tentative course not_meet_prereqs = set() + """ + this readibility sucsk + """ for c in unlocked: """ Get prerequisites for course c using the same logic as the url_for(api_bp.prereqs()) API route diff --git a/app/templates/index.html b/app/templates/index.html index 29c1154..d5d5574 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -22,7 +22,7 @@

Tip

You can also enter a lower-level course you've already taken as a target to open show a wider range of course unlocks!

-
+

Target Course

@@ -136,16 +136,70 @@

Semester & View Options

updateButtonStates(); // Basic form validation - document.querySelector('form').addEventListener('submit', function(e) { + const CODE_RE = /^[A-Z]{3,4}\d{4}[A-Z]?/; + + document.getElementById('temp-form-name').addEventListener('submit', function(e) { + e.preventDefault(); + const tentativeCode = document.getElementById('tentative-code').value.trim(); - if (!tentativeCode) { - e.preventDefault(); + if (!CODE_RE.test(tentativeCode)) { const el = document.getElementById('tentative-code'); el.focus(); el.style.borderColor = '#e74c3c'; - setTimeout(() => { el.style.borderColor = '#e1e5e9'; }, 3000); + alert("Invalid course code(s)") + console.log('Invalid tentative code input:', tentativeCode); + return; } + + const validCourses = []; + const courseInputs = document.querySelectorAll('#course-inputs input'); + + for (const input of courseInputs) { + const value = input.value.trim(); + if (value) { // Only process non-empty values + if (!CODE_RE.test(value)) { + input.focus(); + input.style.borderColor = '#e74c3c'; + alert("Invalid course code(s)"); + console.log('Invalid course input:', value); + return; + } + validCourses.push(value.toUpperCase()); + } + } + + const formData = new FormData(); + formData.append('tentative-code', tentativeCode.toUpperCase()); + formData.append('completed-courses', JSON.stringify(validCourses)); + formData.append('semester', document.getElementById('semester').value); + formData.append('view_type', document.getElementById('view_type').value); + + fetch(this.action, { + method: 'POST', + body: formData + }).then(response => { + if (response.ok) { + window.location.href = response.url; + } else { + alert('Submission failed'); + } + }).catch(error => { + console.error('Error:', error); + alert('Submission failed'); + }); }); + + // Reset input styling when user interacts with any input + document.addEventListener('input', function(e) { + if (e.target.tagName === 'INPUT') { + e.target.style.borderColor = ''; + } + }); + document.addEventListener('input', function(e) { + if (e.target.tagName === 'INPUT') { + e.target.style.borderColor = ''; + } + }, true); From 1d07df0192225eed3efe73a1a7be0ce815e7d2bb Mon Sep 17 00:00:00 2001 From: aleguy02 Date: Sun, 24 Aug 2025 13:22:02 -0400 Subject: [PATCH 2/6] Store completed courses in a cookie instead of query param --- app/routes/main.py | 38 ++++++++++++++++++-------------------- app/templates/index.html | 6 +++--- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/routes/main.py b/app/routes/main.py index c9c3a4e..b914796 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -10,14 +10,14 @@ current_app, request, abort, + make_response, ) from app.config import get_config -import json +import re, json main_bp: Blueprint = Blueprint("main", __name__) config = get_config() -import re CODE_RE = re.compile(r"^[A-Z]{3,4}\d{4}[A-Z]?") @@ -50,31 +50,25 @@ def index(): @main_bp.route("/unlocks", methods=["POST"]) def unlocks_redirect(): code = request.form.get("tentative-code", "") - completed = request.form.get(f"completed-courses", "[]") - try: - completed = json.loads(completed) - except json.JSONDecodeError as e: - current_app.logger.exception(f"Error decoding JSON: {e}") - completed = [] - sem = request.form.get("semester", current_app.config["DEFAULT_SEMESTER"]) view = request.form.get("view_type", "") if view not in ("tcm", "graph"): abort(400, "Bad view type") - return redirect( - url_for( - "main.unlocks_page", - code=code, - completed=_to_CSV( - completed - ), # optimizaton, pass more efficiently, maybe in a cookie - semester=sem, - view_type=view, + resp = make_response( + redirect( + url_for( + "main.unlocks_page", + code=code, + semester=sem, + view_type=view, + ) ) ) + resp.set_cookie("completed-courses", completed) + return resp @main_bp.route("/unlocks/") @@ -83,8 +77,12 @@ def unlocks_page(code: str): if not base: abort(400, f"Bad course code: {code}") - completed_raw = request.args.get("completed", "") - completed = set(completed_raw.split(",")) if completed_raw else set() + completed = request.cookies.get("completed-courses") + try: + completed = set(json.loads(completed)) + except json.JSONDecodeError as e: + current_app.logger.exception(f"Error decoding JSON: {e}") + completed = set() sem = request.args.get("semester", current_app.config["DEFAULT_SEMESTER"]) view = request.args.get("view_type", "") diff --git a/app/templates/index.html b/app/templates/index.html index d5d5574..33b2ec8 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -151,7 +151,7 @@

Semester & View Options

return; } - const validCourses = []; + const validCourses = new Set(); const courseInputs = document.querySelectorAll('#course-inputs input'); for (const input of courseInputs) { @@ -164,13 +164,13 @@

Semester & View Options

console.log('Invalid course input:', value); return; } - validCourses.push(value.toUpperCase()); + validCourses.add(value.toUpperCase()); } } const formData = new FormData(); formData.append('tentative-code', tentativeCode.toUpperCase()); - formData.append('completed-courses', JSON.stringify(validCourses)); + formData.append('completed-courses', JSON.stringify(Array.from(validCourses))); formData.append('semester', document.getElementById('semester').value); formData.append('view_type', document.getElementById('view_type').value); From 1cc2570585cfacb734b23c1ff3cf15aa8cc55580 Mon Sep 17 00:00:00 2001 From: aleguy02 Date: Sun, 24 Aug 2025 14:20:28 -0400 Subject: [PATCH 3/6] Auto populate form data with cookie info in frontend --- app/routes/main.py | 11 +++++++++++ app/templates/index.html | 26 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/routes/main.py b/app/routes/main.py index b914796..e9e1356 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -38,12 +38,23 @@ def index(): """ Returns home page """ + completed = request.cookies.get("completed-courses") + try: + completed = set(json.loads(completed)) + except json.JSONDecodeError as e: + current_app.logger.exception(f"Error decoding JSON: {e}") + completed = set() + + # Convert to a sorted list for stable JSON serialization in template (sets are not JSON serializable) + completed_list = sorted(completed) + return render_template( "index.html", title="GraphUF", semesters=current_app.config["SEMESTERS"], default_semester=current_app.config["DEFAULT_SEMESTER"], max_courses_taken=config.MAX_COURSES_TAKEN, + completed_courses=completed_list, ) diff --git a/app/templates/index.html b/app/templates/index.html index 33b2ec8..d202553 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -87,6 +87,8 @@

Semester & View Options