-
Notifications
You must be signed in to change notification settings - Fork 0
Chore/improve completed courses ux #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b23fdfb
1d07df0
1cc2570
af811ac
97e5abc
637118a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,13 +10,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| current_app, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| request, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| abort, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| make_response, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.config import get_config | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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]?") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,50 +38,48 @@ def index(): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns home page | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| completed = request.cookies.get("completed-courses", "[]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| completed = set(json.loads(completed)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @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")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| code = request.form.get("tentative-code", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| completed = request.form.get(f"completed-courses", "[]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove unnecessary f-string and validate client-provided JSON before use.
Apply this diff: - code = request.form.get("tentative-code", "")
- completed = request.form.get(f"completed-courses", "[]")
+ code = request.form.get("tentative-code", "")
+ completed_raw = request.form.get("completed-courses", "[]")
+ try:
+ items = json.loads(completed_raw)
+ if not isinstance(items, list):
+ raise ValueError("completed-courses must be a list")
+ # Normalise, dedupe, and keep stable ordering
+ norm = {normalise(c) for c in items if isinstance(c, str) and normalise(c)}
+ completed = json.dumps(sorted(norm))
+ except (json.JSONDecodeError, TypeError, ValueError) as e:
+ current_app.logger.warning("Invalid completed-courses in form: %s", e)
+ completed = "[]"📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.12.2)64-64: f-string without any placeholders Remove extraneous (F541) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return redirect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| url_for( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "main.unlocks_page", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| code=code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| completed=_to_CSV(completed), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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/<code>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,8 +88,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 Exception 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", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -114,6 +117,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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,7 +22,7 @@ <h4><i class="fas fa-info-circle"></i>Tip</h4> | |||||||||||||||||||||||||||||||||||||||||||||
| <p>You can also enter a lower-level course you've already taken as a target to open show a wider range of course unlocks!</p> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <form action="{{ url_for('main.unlocks_redirect') }}" method="post"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <form id="temp-form-name" action="{{ url_for('main.unlocks_redirect') }}" method="post"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="form-section"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <h3><i class="fas fa-target"></i>Target Course</h3> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="form-group"> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -87,6 +87,8 @@ <h3><i class="fas"></i>Semester & View Options</h3> | |||||||||||||||||||||||||||||||||||||||||||||
| <div id="config" data-max-courses="{{ max_courses_taken }}" hidden></div> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <script> | ||||||||||||||||||||||||||||||||||||||||||||||
| // Server-provided previously completed courses (from cookie) | ||||||||||||||||||||||||||||||||||||||||||||||
| const PRECOMPLETED = JSON.parse('{{ completed_courses | tojson | safe }}'); | ||||||||||||||||||||||||||||||||||||||||||||||
| const configEl = document.getElementById('config'); | ||||||||||||||||||||||||||||||||||||||||||||||
| const maxCourses = parseInt(configEl.dataset.maxCourses, 10); | ||||||||||||||||||||||||||||||||||||||||||||||
| const minCourses = 5; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,20 +134,96 @@ <h3><i class="fas"></i>Semester & View Options</h3> | |||||||||||||||||||||||||||||||||||||||||||||
| addBtn.disabled = currentCount >= maxCourses; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Initial state | ||||||||||||||||||||||||||||||||||||||||||||||
| // Helper to ensure at least n inputs exist (adding in step-sized batches without exceeding max) | ||||||||||||||||||||||||||||||||||||||||||||||
| function ensureInputCount(n) { | ||||||||||||||||||||||||||||||||||||||||||||||
| let currentCount = courseInputsDiv.querySelectorAll('input').length; | ||||||||||||||||||||||||||||||||||||||||||||||
| while (currentCount < n) { | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 5; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||
| courseInputsDiv.appendChild(createCourseInput(currentCount + 1)); | ||||||||||||||||||||||||||||||||||||||||||||||
| currentCount++; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+137
to
+146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion ensureInputCount ignores step/max and can over-append inputs. Respect step and clamp to maxCourses to prevent runaway DOM growth when PRECOMPLETED is large. Apply this diff: - // Helper to ensure at least n inputs exist (adding in step-sized batches without exceeding max)
- function ensureInputCount(n) {
- let currentCount = courseInputsDiv.querySelectorAll('input').length;
- while (currentCount < n) {
- for (let i = 0; i < 5; i++) {
- courseInputsDiv.appendChild(createCourseInput(currentCount + 1));
- currentCount++;
- }
- }
- }
+ // Helper to ensure at least n inputs exist (respecting step and max)
+ function ensureInputCount(n) {
+ const target = Math.min(n, maxCourses);
+ let currentCount = courseInputsDiv.querySelectorAll('input').length;
+ while (currentCount < target) {
+ const toAdd = Math.min(step, target - currentCount);
+ for (let i = 0; i < toAdd; i++) {
+ courseInputsDiv.appendChild(createCourseInput(currentCount + 1));
+ currentCount++;
+ }
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Populate any pre-completed courses from cookie | ||||||||||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(PRECOMPLETED) && PRECOMPLETED.length) { | ||||||||||||||||||||||||||||||||||||||||||||||
| ensureInputCount(Math.max(minCourses, PRECOMPLETED.length)); | ||||||||||||||||||||||||||||||||||||||||||||||
| const inputs = courseInputsDiv.querySelectorAll('input'); | ||||||||||||||||||||||||||||||||||||||||||||||
| PRECOMPLETED.forEach((code, idx) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (idx < inputs.length) { | ||||||||||||||||||||||||||||||||||||||||||||||
| inputs[idx].value = code; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Initial state after potential population | ||||||||||||||||||||||||||||||||||||||||||||||
| 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 = new Set(); | ||||||||||||||||||||||||||||||||||||||||||||||
| 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.add(value.toUpperCase()); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||||
| formData.append('tentative-code', tentativeCode.toUpperCase()); | ||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||
| </body> | ||||||||||||||||||||||||||||||||||||||||||||||
| </html> | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Add tests for cookie handling edge cases.
Cover: no cookie, invalid JSON (e.g., "5"), non-list JSON (e.g., "{}"), mixed/invalid items, lowercase and spaced codes → all should not crash and should normalize consistently.
Run this quick check to find all cookie reads that must tolerate None/invalid JSON:
Also applies to: 91-96
🏁 Script executed:
Length of output: 928
Unify and Harden Cookie Handling; Add Edge-Case Tests
The two
completed-coursescookie reads in app/routes/main.py are handled inconsistently and may still crash on missing or malformed input. We should:"[]") to avoidNone→json.loads(None)errorsjson.JSONDecodeErrorandTypeError(the latter arises whencompletedisNone)"5")"{}")Locations needing updates:
cookies.get("completed-courses", "[]"), but confirm exception handling covers all error types.request.cookies.get("completed-courses", "[]")and extendexceptto catchTypeErroras well.Suggested shell check to find all occurrences:
Let me know if you’d like help writing those tests or hardening the exception logic.
🤖 Prompt for AI Agents