def present_queue_errors(self): error_rows = _WorkEvent.queue_errors if not error_rows: return True path_input = ui.layers.PathInputLayer("Queue Data Errors") path_input.set_prompt([ "There were some queue data entries that caused errors.", "Select a file to write them to so that" " you can manually adjust the stats for them.", "", # empty line "If you find common errors, please open an issue on the" " github repo, or talk to a maintainer directly.", "" # empty line ]) default_path = os.path.join(preferences.get("output_dir"), "bad-queue-data.csv") path_input.set_text(default_path) ui.get_window().run_layer(path_input) if path_input.canceled: return False with open(path_input.get_path(), "w", newline="") as out_file: writer = csv.writer(out_file) writer.writerows(error_rows) return True
def _select_time(title: str, default_time: datetime.time): time_selector = ui.layers.DatetimeSpinner(title) time_selector.set_quickpicks([(59, 59), (0, 0)]) initial_time = datetime.datetime.combine(datetime.datetime.today(), default_time) time_selector.set_initial_time(initial_time) ui.get_window().run_layer(time_selector, "Bob's Shake") return (None if time_selector.canceled else time_selector.get_time())
def select_help_queue_data_file(self): filepath_entry = ui.layers.PathInputLayer("Help Queue Data") filepath_entry.set_prompt([ "Enter the path to the queue data", "(you should have copy-pasted the data from the queue into a file)", "" # empty line ]) ui.get_window().run_layer(filepath_entry) if filepath_entry.canceled: return False self.help_queue_csv_filepath = filepath_entry.get_path() return True
def validate_queue_names(self): """Make sure each TA name from the queue has a known netid""" window = ui.get_window() used_qnames = {event.ta_name for event in self.queuee_events} stored_tas = data.get_tas().copy() stored_netids = {ta.netid: ta for ta in stored_tas} stored_qnames = {ta.queue_name for ta in stored_tas} unknown_qnames = used_qnames - stored_qnames netid_input = ui.layers.TextInputLayer("Unknown Name in Queue Data") for qname in unknown_qnames: netid_input.set_prompt([ f"There is no stored TA with the name {qname}.", f"Please enter the netid for {qname}." ]) netid_input.set_text("") window.run_layer(netid_input) if netid_input.canceled: return False netid = netid_input.get_text() if netid in stored_netids: stored_netids[netid].queue_name = qname else: new_ta = data.model.TA(netid, qname) stored_netids[netid] = new_ta stored_tas.append(new_ta) data.write_tas(stored_tas) return True
def remove_locks(): window = ui.get_window() all_locks = {lock: False for lock in data.lock.get_lock_files()} popup = ui.layers.ListLayer("Select Locks to Remove", popup=True) popup.set_exit_text("Confirm") for lock in all_locks: popup.add_row_toggle(lock, LockToggle(lock, all_locks)) window.run_layer(popup) selected_locks = [lock for lock in all_locks if all_locks[lock]] if not selected_locks: return # Confirm popup = ui.layers.BoolPopup("Confirm Removal") popup.set_message( [f"Are you sure you want to remove {len(selected_locks)} lock(s)?"]) window.run_layer(popup) if not popup.get_result() or popup.canceled: return # Remove selected locked content for lock in selected_locks: if lock: data.lock.remove_lock_file(lock)
def prep_lab_score_calc(): """A simple calculator for determining the score for a late prep lab""" window = ui.get_window() try: text_input = ui.layers.TextInputLayer("Original Score") text_input.set_prompt(["What was the student's original score?"]) window.run_layer(text_input, "Prep Lab Calculator") if text_input.canceled: return old_score = float(text_input.get_text()) text_input = ui.layers.TextInputLayer("zyBooks Completion") text_input.set_prompt( ["What is the student's current completion % in zyBooks"]) text_input.set_text("100") window.run_layer(text_input, "Prep Lab Calculator") if text_input.canceled: return # Calculate the new score current_completion = float(text_input.get_text()) new_score = old_score + ((current_completion - old_score) * 0.6) popup = ui.layers.Popup("New Score") popup.set_message([f"The student's new score is: {new_score}"]) window.run_layer(popup, "Prep Lab Calculator") except ValueError: popup = ui.layers.Popup("Error") popup.set_message(["Invalid input"]) window.run_layer(popup, "Prep Lab Calculator")
def lab_manager(): window = ui.get_window() menu = ui.layers.ListLayer() menu.add_row_text("Add Lab", add_lab) menu.add_row_text("Edit Current Labs", edit_labs) window.register_layer(menu, "Lab Manager")
def edit_class_sections(): """Create list of class sections to edit""" window = ui.get_window() section_list = ui.layers.ListLayer() fill_class_section_list(section_list) window.register_layer(section_list)
def preference_update_fn(): """Callback that is run when preferences are updated""" window = ui.get_window() window.update_preferences() events = ui.get_events() events.update_preferences()
def setup_new_class(): """Setup a new class based on a zyBooks class code""" window = ui.get_window() zy_api = Zybooks() text_input = ui.layers.TextInputLayer("Class Code") text_input.set_prompt(["Enter class code"]) window.run_layer(text_input) if text_input.canceled: return # Check if class code is valid code = text_input.get_text() valid = zy_api.check_valid_class(code) if valid: popup = ui.layers.Popup("Valid", [f"{code} is valid"]) window.run_layer(popup) else: popup = ui.layers.Popup("Invalid", [f"{code} is invalid"]) window.run_layer(popup) return # If code is valid, add it to the shared configuration SharedData.add_class(code) # Download the list of students roster = zy_api.get_roster() save_roster(roster) popup = ui.layers.Popup("Finished", ["Successfully downloaded student roster"]) window.run_layer(popup) class_section_manager()
def set_due_date(lab, row: ui.layers.Row): """Set a cutoff date for a lab When grading the submission before the cutoff date will be shown, but the in-grader submission picker allows to pick submissions after the cutoff date if needed """ window = ui.get_window() labs = data.get_labs() old_date = lab.options.get("due", None) date_spinner = ui.layers.DatetimeSpinner("Due Date") date_spinner.set_optional(True) if old_date: date_spinner.set_initial_time(old_date) window.run_layer(date_spinner) if date_spinner.canceled: return due_date = date_spinner.get_time() # Clearing the due date if due_date == ui.components.DatetimeSpinner.NO_DATE: if "due" in lab.options: del lab.options["due"] else: # Remove time zone information lab.options["due"] = due_date.astimezone(tz=None) data.write_labs(labs) set_date_text(lab, row)
def get_submission(lab, student, use_locks=True): """Get a submission from zyBooks given the lab and student""" window = ui.get_window() zy_api = Zybooks() # Lock student if use_locks: data.lock.lock(student, lab) submission_response = zy_api.download_assignment(student, lab) submission = data.model.Submission(student, lab, submission_response) # Report missing files if submission.flag & data.model.SubmissionFlag.BAD_ZIP_URL: msg = [ f"One or more URLs for {student.full_name}'s code submission are bad.", "Some files could not be downloaded. Please", "View the most recent submission on zyBooks.", ] popup = ui.layers("Warning", msg) window.run_layer(popup) # A student may have submissions beyond the due date, and an exception # In case that happens, always allow a normal grade, but show a message if submission.flag == data.model.SubmissionFlag.NO_SUBMISSION: pass return submission
def lab_select_fn(selected_index, use_locks, student: model.Student = None): """Callback function that executes after selecting a lab""" lab = data.get_labs()[selected_index] # Skip selecting a student and go immediately to the grader if student: student_select_fn(student, lab, use_locks) return window = ui.get_window() students = data.get_students() student_list = ui.layers.ListLayer() student_list.set_searchable("Student") student_list.set_sortable() fill_student_list(student_list, students, lab, use_locks, student_select_fn) # Register a watch function to watch the students watch_students(student_list, students, lab, use_locks) # # Remove the file watch handler when done choosing students student_list.set_destroy_fn( lambda: data.fs_watch.fs_watch_unregister("student_list_watch")) window.register_layer(student_list, lab.name)
def edit_labs(): """Creates a list of labs to edit""" window = ui.get_window() labs = data.get_labs() lab_list = ui.layers.ListLayer() fill_lab_list(lab_list, labs) window.register_layer(lab_list)
def report_gaps(): """Report any cells in the gradebook that do not have a score""" window = ui.get_window() if not _confirm_gradebook_ready(): return # Use the Canvas parsing from the gradepuller to get the gradebook in puller = grade_puller.GradePuller() try: puller.read_canvas_csv() except grade_puller.GradePuller.StoppingException: return real_assignment_pattern = re.compile(r".*\([0-9]+\)") # Create mapping from assignment names to lists of students # with no grade for that assignment all_gaps = dict() for assignment in puller.canvas_header: if real_assignment_pattern.match(assignment): gaps = [] for student in puller.canvas_students.values(): if not student[assignment]: gaps.append(student['Student']) if gaps: all_gaps[assignment] = gaps # Abort if no gaps present if not all_gaps: popup = ui.layers.Popup("Full Gradebook", ["There are no gaps in the gradebook"]) window.run_layer(popup) return # Transpose the data for easier reading rows = [list(all_gaps.keys())] added = True while added: added = False new_row = [] for assignment in rows[0]: if all_gaps[assignment]: new_row.append(all_gaps[assignment].pop(0)) added = True else: new_row.append("") rows.append(new_row) # select the output file and write to it out_path = filename_input(purpose="the gap report", text=os.path.join(preferences.get("output_dir"), "gradebook_gaps.csv")) if out_path is None: return with open(out_path, "w", newline="") as out_file: writer = csv.writer(out_file) writer.writerows(rows)
def grade_pair_programming(first_submission, use_locks): """Pick a second student to grade pair programming with""" # Get second student window = ui.get_window() students = data.get_students() lab = first_submission.lab student_list = ui.layers.ListLayer() student_list.set_searchable("Student") student_list.set_sortable() fill_student_list(student_list, students, lab, use_locks) window.run_layer(student_list) if student_list.canceled: return # Get student student_index = student_list.selected_index() student = students[student_index] if not can_get_through_locks(use_locks, student, lab): return try: second_submission = get_submission(lab, student, use_locks) if second_submission is None: return if second_submission == first_submission: popup = ui.layers.Popup( "Invalid Student", ["The first and second students are the same"]) window.run_layer(popup) return first_submission_fn = lambda: pair_programming_submission_callback( lab, first_submission) second_submission_fn = lambda: pair_programming_submission_callback( lab, second_submission) msg = lambda: pair_programming_message(first_submission, second_submission) popup = ui.layers.OptionsPopup("Pair Programming") popup.set_message(msg) popup.add_option(first_submission.student.full_name, first_submission_fn) popup.add_option(second_submission.student.full_name, second_submission_fn) popup.add_option( "View Diff", lambda: view_diff(first_submission, second_submission)) window.run_layer(popup) finally: if use_locks: data.lock.unlock(student, lab)
def shake(): """Read TA work data, "shake" it, and report the shaken statistics. Results summarize work in grading, answering emails, and helping students. For each category of work, Bob's Shake reports the number of items finished by a TA and the total time spent on those items. Resulting summary statistics are written to a file chosen by the user. Some TA data is read from zygrader's own native lock log, and additional data is read from a csv file created by our help queue. "shaking" occurs in a series of steps, roughly: - user selects start and end time to include - user points Bob to the data from the help queue - data is read from zygrader's log and the queue info - errors encountered and corrections that need to be made are presented to the user - input data is assigned to each TA appropriately - each TA's events are analyzed to calculate the results summary - user selects an output file - the summary statistics are written to the file """ window = ui.get_window() wait_popup = ui.layers.WaitPopup("Bob's Shake") worker = _StatsWorker() Step = namedtuple('Step', ['interactive', 'msg', 'func']) steps = [ Step(True, None, worker.select_start_time), Step(True, None, worker.select_end_time), Step(True, None, worker.select_help_queue_data_file), Step(False, "Read data from the log file", worker.read_in_native_stats), Step(False, "Read data from help queue file", worker.read_in_help_queue_stats), Step(True, None, worker.present_queue_errors), Step(True, None, worker.validate_queue_names), Step(False, "Assign events to individual tas", worker.assign_events_to_tas), Step(False, "Analyze stats for each TA", worker.analyze_tas_individually), Step(True, None, worker.select_output_file), Step(False, "Write shaken stats to file", worker.write_stats_to_file) ] _WorkEvent.queue_errors = [] for step in steps: if step.interactive: if not step.func(): return else: wait_popup.set_message([step.msg]) wait_popup.set_wait_fn(step.func) window.run_layer(wait_popup) if wait_popup.canceled: return
def sort_class_sections(): class_sections = data.get_class_sections() class_sections = sorted(class_sections, key=lambda sec: sec.section_number) data.write_class_sections(class_sections) window = ui.get_window() popup = ui.layers.Popup( "Finished", ["The Class Sections are now sorted by section number"]) window.run_layer(popup)
def class_section_manager(): window = ui.get_window() menu = ui.layers.ListLayer() menu.add_row_text("Add Section", add_class_section) menu.add_row_text("Remove Section", remove_class_section) menu.add_row_text("Edit Current Sections", edit_class_sections) menu.add_row_text("Sort Current Sections", sort_class_sections) window.register_layer(menu, "Class Section Manager")
def submission_search_init(): """Get lab part and string from the user for searching""" window = ui.get_window() labs = data.get_labs() menu = ui.layers.ListLayer() menu.set_searchable("Assignment") for lab in labs: menu.add_row_text(str(lab)) window.run_layer(menu, "Submissions Search") if menu.canceled: return assignment = labs[menu.selected_index()] # Select the lab part if needed if len(assignment.parts) > 1: popup = ui.layers.ListLayer("Select Part", popup=True) for part in assignment.parts: popup.add_row_text(part["name"]) window.run_layer(popup, "Submissions Search") if popup.canceled: return part = assignment.parts[popup.selected_index()] else: part = assignment.parts[0] regex_input = ui.layers.BoolPopup("Use Regex") regex_input.set_message(["Would you like to use regex?"]) window.run_layer(regex_input) if regex_input.canceled: return use_regex = regex_input.get_result() text_input = ui.layers.TextInputLayer("Search String") text_input.set_prompt(["Enter a search string"]) window.run_layer(text_input, "Submissions Search") if text_input.canceled: return search_string = text_input.get_text() # Get a valid output path filename_input = ui.layers.PathInputLayer("Output File") filename_input.set_prompt( ["Enter the filename to save the search results"]) filename_input.set_text(preferences.get("output_dir")) window.run_layer(filename_input, "Submissions Search") if filename_input.canceled: return logger = ui.layers.LoggerLayer() logger.set_log_fn(lambda: submission_search_fn( logger, part, search_string, filename_input.get_path(), use_regex)) window.run_layer(logger, "Submission Search")
def fetch_zybooks_toc(): window = ui.get_window() zy_api = Zybooks() popup = ui.layers.WaitPopup("Table of Contents", ["Fetching TOC from zyBooks"]) popup.set_wait_fn(zy_api.get_table_of_contents) window.run_layer(popup) return popup.get_result()
def end_of_semester_tools(): """Create the menu for end of semester tools""" window = ui.get_window() menu = ui.layers.ListLayer() menu.add_row_text("Report Gaps", report_gaps) menu.add_row_text("Midterm Mercy", midterm_mercy) menu.add_row_text("Attendance Score", attendance_score) window.register_layer(menu)
def _confirm_gradebook_ready(): window = ui.get_window() confirmation = ui.layers.BoolPopup("Using canvas_master", [ "This operation requires an up-to date canvas_master.", ("Please confirm that you have downloaded the gradebook" " and put it in the right place."), "Have you done so?" ]) window.run_layer(confirmation) return (not confirmation.canceled) and confirmation.get_result()
def view_changelog(): window = ui.get_window() lines = config.versioning.load_changelog() popup = ui.layers.ListLayer("Changelog", popup=True) popup.set_exit_text("Press enter to close") popup.set_paged() for line in lines: popup.add_row_text(line) window.run_layer(popup, "Changelog")
def start(): """Create the main class manager menu""" window = ui.get_window() menu = ui.layers.ListLayer() menu.add_row_text("Setup New Class", setup_new_class) menu.add_row_text("Lab Manager", lab_manager) menu.add_row_text("Class Section Manager", class_section_manager) menu.add_row_text("Download Student Roster", download_roster) menu.add_row_text("Change Class", change_class) window.register_layer(menu, "Class Manager")
def edit_labs_fn(lab, lab_list: ui.layers.ListLayer): """Create a popup for basic lab editing options""" window = ui.get_window() msg = [f"Editing {lab.name}", "", "Select an option"] popup = ui.layers.OptionsPopup("Edit Lab", msg) popup.add_option("Remove", lambda: remove_fn(lab_list, window, lab)) popup.add_option("Rename", lambda: rename_lab(lab_list, lab)) popup.add_option("Move Up", lambda: move_lab(lab_list, lab, -1)) popup.add_option("Move Down", lambda: move_lab(lab_list, lab, 1)) popup.add_option("Edit Options", lambda: edit_lab_options(lab)) window.register_layer(popup)
def edit_lab_options(lab): window = ui.get_window() popup = ui.layers.ListLayer("Edit Lab Options", popup=True) popup.add_row_toggle("Grade Highest Scoring Submission", LabOptionToggle(lab, "highest_score")) popup.add_row_toggle("Diff Submission Parts", LabOptionToggle(lab, "diff_parts")) row = popup.add_row_text("Due Date") row.set_callback_fn(set_due_date, lab, row) set_date_text(lab, row) window.register_layer(popup)
def filename_input(purpose, text=""): """Get a valid filename from the user""" window = ui.get_window() path_input = ui.layers.PathInputLayer("Filepath Entry") path_input.set_prompt( [f"Enter the path and filename for {purpose} [~ is supported]"]) path_input.set_text(text) window.run_layer(path_input) if path_input.canceled: return None return path_input.get_path()
def email_menu(): """Show the list of students with auto-update and locking.""" window = ui.get_window() students = data.get_students() student_list = ui.layers.ListLayer() student_list.set_searchable("Student") student_list.set_sortable() fill_student_list(student_list, students) watch_students(student_list, students) student_list.set_destroy_fn( lambda: data.fs_watch.fs_watch_unregister("student_email_list_watch")) window.register_layer(student_list, "Email Manager")
def rename_lab(lab_list: ui.layers.ListLayer, lab): """Rename a lab""" window = ui.get_window() labs = data.get_labs() text_input = ui.layers.TextInputLayer("Rename Lab") text_input.set_prompt(["Enter Lab's new name"]) text_input.set_text(lab.name) window.run_layer(text_input) if not text_input.canceled: lab.name = text_input.get_text() data.write_labs(labs) fill_lab_list(lab_list, labs)