diff --git a/courses.py b/courses.py index 3239f1f..1cb88f6 100644 --- a/courses.py +++ b/courses.py @@ -2424,11 +2424,9 @@ def remove_n_analytics(section=0): import csv def my_nav_filter(row): - # Return True for tabs that should be considered ON for reporting. - # A nav item is ON if visibility is 'public' and 'hidden' is n/a or False. + # Filter logic: consider a tab ON only when visibility is public and not hidden; used for CSV summary. if str(row.get('visibility', '')).lower() != 'public': return False - # Treat missing/"n/a" as not hidden hidden = row.get('hidden') if str(hidden).lower() in ['true']: return False @@ -2437,7 +2435,7 @@ def my_nav_filter(row): ## Multi-pass course nav tool. Pass 1: index + XLSX; Pass 2: optional hide/show ids. def clean_course_nav_setup_semester(section=0): - # Multi-pass course nav tool: Pass 1 indexes tabs + writes XLSX matrix; Pass 2 optionally hides/shows selected tab ids across all courses in the term. + # Multi-pass tool: index course nav, write XLSX with x/-/blank, then optionally bulk hide/show by label across the term. TESTING = False # Limit to first 10 courses while testing import openpyxl from openpyxl.utils import get_column_letter @@ -2454,13 +2452,13 @@ def clean_course_nav_setup_semester(section=0): print("Fetching list of all active courses") courses_in_term = getCoursesInTerm(term, 1, 0) if TESTING: - print("TESTING mode enabled: limiting to first 10 courses") - courses_in_term = courses_in_term[:10] + print("TESTING mode enabled: limiting to first 20 courses") + courses_in_term = courses_in_term[:20] # Collect course records and their tabs all_tabs_by_course = {} # course_id -> list of tab dicts all_tab_ids = {} # tab_id -> label (most recent seen) — kept for Pass 2 id operations - any_on_labels = set() # labels that are ON in at least one course + all_labels = set() # all labels seen in any course (visible or hidden) # Also write a detailed CSV of visible tabs as before nav_out = codecs.open(f'cache/course_nav_summary_{SEM}.csv', 'w', 'utf-8') @@ -2480,6 +2478,8 @@ def clean_course_nav_setup_semester(section=0): T['hidden'] = "n/a" # Track global set of tab ids and a sample label all_tab_ids[str(T.get('id'))] = T.get('label', str(T.get('id'))) + if T.get('label'): + all_labels.add(T.get('label')) # Write summary of ON tabs vals = [C['id'], C['name'], C['course_code'], C.get('start_at', ''), C.get('workflow_state', ''), T.get('label', ''), T.get('position', ''), T.get('hidden', ''), T.get('visibility', ''), @@ -2487,17 +2487,53 @@ def clean_course_nav_setup_semester(section=0): mydict = dict(zip(columns, vals)) if my_nav_filter(mydict): nav_writer.writerow(vals) - if T.get('label'): - any_on_labels.add(T.get('label')) nav_out.flush() all_tabs_by_course[cid] = tabs except Exception as err: print(f"Exception: {err}") + try: + nav_out.close() + except Exception: + pass + # Build XLSX matrix try: - # Column order: labels with at least one ON occurrence, alphabetical - tab_labels = sorted(any_on_labels, key=lambda x: str(x).lower()) + # Compute popularity (count of visible 'x' occurrences per label) and sort by popularity then alpha. + # First, build quick lookup per course for label visibility/hidden. + def tab_status(t): + vis = str(t.get('visibility', '')).lower() + hid = str(t.get('hidden', '')).lower() + if vis == 'public' and hid not in ['true']: + return 'x' # present and visible + return '-' # present but hidden (includes non-public visibility) + + # Precompute per-course map: label -> status symbol ('x' or '-') + status_by_course = {} + for C in courses_in_term: + cid = str(C['id']) + label_to_status = {} + for tdict in all_tabs_by_course.get(cid, []) or []: + lbl = tdict.get('label') + if not lbl: + continue + cur = label_to_status.get(lbl) + new = tab_status(tdict) + # If any instance is visible, prefer 'x' over '-' + if cur == 'x': + continue + label_to_status[lbl] = 'x' if new == 'x' else (cur or '-') + status_by_course[cid] = label_to_status + + # Popularity counts + visible_counts = defaultdict(int) + for cid, lmap in status_by_course.items(): + for lbl, sym in lmap.items(): + if sym == 'x': + visible_counts[lbl] += 1 + + # Column order: by decreasing x-count, then alphabetical for ties + tab_labels = sorted(all_labels, key=lambda s: (-visible_counts.get(s, 0), str(s).lower())) xlsx_path = f"cache/course_nav_matrix_{SEM}.xlsx" wb = openpyxl.Workbook() ws = wb.active @@ -2505,8 +2541,8 @@ def clean_course_nav_setup_semester(section=0): # Headers static_headers = ['course_id', 'course_name', 'first_teacher'] - # Row 1: labels only (ids omitted per request) - row1 = static_headers + tab_labels + # Row 1: labels only, sorted by popularity (most x's first) + row1 = static_headers + list(tab_labels) ws.append(row1) # Rows: one per course @@ -2524,18 +2560,10 @@ def clean_course_nav_setup_semester(section=0): pass row = [cid, cname, teacher] - # Build a set of labels that are ON for this course - on_labels_in_course = set() - for tdict in all_tabs_by_course.get(cid, []) or []: - visibility = str(tdict.get('visibility', '')).lower() - hidden_val = tdict.get('hidden', '') - hidden_str = str(hidden_val).lower() - if visibility == 'public' and hidden_str not in ['true']: - lbl = tdict.get('label') - if lbl: - on_labels_in_course.add(lbl) + course_map = status_by_course.get(cid, {}) for lbl in tab_labels: - row.append('x' if lbl in on_labels_in_course else '') + sym = course_map.get(lbl, '') + row.append(sym) ws.append(row) # Simple sizing for readability @@ -2548,88 +2576,83 @@ def clean_course_nav_setup_semester(section=0): except Exception as ex: print(f"Failed to write XLSX matrix: {ex}") - # Optional Pass 2: apply hide/show updates (by id or label) - try_apply = input("Apply changes? [n] hide/show selected tab ids or labels across all courses (y/N): ").strip().lower() in ['y', 'yes'] + # Optional Pass 2: apply hide/show updates (labels only) + try_apply = input("Apply changes? [n] hide/show selected labels across all courses (y/N): ").strip().lower() in ['y', 'yes'] if not try_apply: print("Pass 1 complete. No changes applied.") return - # Gather ids to HIDE and SHOW - def parse_id_list(s): + # Gather labels to HIDE and SHOW + def parse_label_list(s): if not s: return [] - # allow comma/space/newline separated - tokens = [] - for line in s.replace(',', ' ').split(): - tokens.append(line.strip()) - return [x for x in tokens if x] + # Allow comma-separated list. Spaces are preserved within labels. + if '\n' in s: + raw = [x.strip() for x in s.split('\n') if x.strip()] + else: + raw = [x.strip() for x in s.split(',') if x.strip()] + return raw - hide_src = input("Enter tab ids OR labels to HIDE (comma/space separated), or path to file in cache/ (leave blank to skip): ").strip() - show_src = input("Enter tab ids OR labels to SHOW (add/unhide), or path to file in cache/ (leave blank to skip): ").strip() + hide_src = input("Enter labels to HIDE (comma-separated or newline file path), leave blank to skip: ").strip() + show_src = input("Enter labels to SHOW (comma-separated or newline file path), leave blank to skip: ").strip() - hide_ids = [] - show_ids = [] hide_labels = [] show_labels = [] - # file helper - def read_ids_from_path(pth): + + def read_labels_from_path(pth): try: with codecs.open(pth, 'r', 'utf-8') as f: - return parse_id_list(f.read()) + content = f.read() + # Prefer newline separation in files (one label per line) + return [x.strip() for x in content.splitlines() if x.strip()] except Exception: return [] - # Build a set of all labels observed (from all tabs, not only ON) - all_labels_seen = set() - for _cid, tlist in all_tabs_by_course.items(): - for t in (tlist or []): - if t.get('label'): - all_labels_seen.add(t.get('label')) - - def split_ids_labels(raw_list): - ids_out, labels_out = [], [] - for tok in raw_list: - if tok in all_tab_ids or re.match(r"^[a-z_]+(_[a-z0-9]+)*$", tok): - # Heuristic: looks like an id (e.g., syllabus, pages, context_external_tool_123) - ids_out.append(tok) - elif tok in all_labels_seen: - labels_out.append(tok) - else: - # Default to label if not recognizable as id - labels_out.append(tok) - return [str(x) for x in ids_out], labels_out - if hide_src: if os.path.exists(hide_src): - toks = read_ids_from_path(hide_src) + hide_labels = read_labels_from_path(hide_src) elif os.path.exists(os.path.join('cache', hide_src)): - toks = read_ids_from_path(os.path.join('cache', hide_src)) + hide_labels = read_labels_from_path(os.path.join('cache', hide_src)) else: - toks = parse_id_list(hide_src) - hide_ids, hide_labels = split_ids_labels(toks) + hide_labels = parse_label_list(hide_src) if show_src: if os.path.exists(show_src): - toks = read_ids_from_path(show_src) + show_labels = read_labels_from_path(show_src) elif os.path.exists(os.path.join('cache', show_src)): - toks = read_ids_from_path(os.path.join('cache', show_src)) + show_labels = read_labels_from_path(os.path.join('cache', show_src)) else: - toks = parse_id_list(show_src) - show_ids, show_labels = split_ids_labels(toks) + show_labels = parse_label_list(show_src) - if not (hide_ids or show_ids or hide_labels or show_labels): - print("No ids provided. Skipping Pass 2.") + # De-duplicate while keeping order roughly intact + hide_labels = list(dict.fromkeys(hide_labels)) + show_labels = list(dict.fromkeys(show_labels)) + + if not (hide_labels or show_labels): + print("No labels provided. Skipping Pass 2.") return - print(f"HIDE ids: {hide_ids}") print(f"HIDE labels: {hide_labels}") - print(f"SHOW ids: {show_ids}") print(f"SHOW labels: {show_labels}") confirm = input("Proceed with updates? (y/N): ").strip().lower() in ['y', 'yes'] if not confirm: print("Aborted. No changes applied.") return + # Build lookup of labels currently ON per course from the CSV summary (to avoid unnecessary hide calls) + on_labels_by_course = defaultdict(set) + try: + csv_path = f'cache/course_nav_summary_{SEM}.csv' + with codecs.open(csv_path, 'r', 'utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + cid = str(row.get('id', '')).strip() + lbl = row.get('label') + if cid and lbl: + on_labels_by_course[cid].add(lbl) + except Exception as ex: + print(f"Warning: could not read CSV summary for ON labels: {ex}") + # Apply changes for C in courses_in_term: cid = str(C['id']) @@ -2638,7 +2661,6 @@ def clean_course_nav_setup_semester(section=0): # Refresh tabs to reduce staleness just before applying if course_tabs is None: course_tabs = fetch(f"{url}/api/v1/courses/{cid}/tabs") or [] - tabs_by_id = {str(t.get('id')): t for t in course_tabs} tabs_by_label = defaultdict(list) for t in course_tabs: lbl = t.get('label') @@ -2653,18 +2675,9 @@ def clean_course_nav_setup_semester(section=0): action = 'HIDE' if hidden_val else 'SHOW' print(f"{action} {cid} {C.get('name','')} -> {tab_id}: {r.status_code}") - # Hide by ids - for tid in hide_ids: - if tid in tabs_by_id: - set_hidden_for_id(tid, True) - - # Show by ids - for tid in show_ids: - if tid in tabs_by_id: - set_hidden_for_id(tid, False) - - # Hide by labels - for lbl in hide_labels: + # Hide by labels (only if label is currently ON per CSV) + labels_to_hide_here = [lbl for lbl in hide_labels if lbl in on_labels_by_course.get(cid, set())] + for lbl in labels_to_hide_here: for t in tabs_by_label.get(lbl, []): tid = str(t.get('id')) set_hidden_for_id(tid, True)