def start(): """Setup before initializing curses""" # Handle Signals signal.signal(signal.SIGINT, sigint_handler) signal.signal(signal.SIGTSTP, sigtstp_handler) signal.signal(signal.SIGHUP, sighup_handler) # Set a short ESC key delay (curses environment variable) os.environ.setdefault("ESCDELAY", "25") args = parse_args() # Check for updates if not args.no_update and not args.install_version: latest_version = updater.get_latest_version() if latest_version != SharedData.VERSION: updater.update_zygrader(latest_version) sys.exit() if args.install_version: updater.install_version(args.install_version) sys.exit() # Setup user configuration preferences.initialize() versioning.versioning_update_preferences() # Handle configuration based args after config has been initialized handle_args(args) # Check for shared data dir data_dir = preferences.get("data_dir") if not data_dir: print("You have not set the shared data directory") print("Please run with the flag --set-data-dir [path]") sys.exit() # Start application and setup data folders if not SharedData.initialize_shared_data(data_dir): sys.exit() # Load data for the current class data.get_students() data.get_labs() # Change directory to the default output dir os.chdir(os.path.expanduser(preferences.get("output_dir"))) name = data.netid_to_name(getpass.getuser()) # Create a zygrader window, callback to main function ui.Window(main, f"zygrader {SharedData.VERSION}", name, args) logger.log("zygrader exited normally")
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 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 remove_fn(lab_list, window, lab) -> bool: """Remove a lab from the list""" msg = [f"Are you sure you want to remove {lab.name}?"] popup = ui.layers.BoolPopup("Confirm", msg) window.run_layer(popup) remove = popup.get_result() if remove: labs = data.get_labs() labs.remove(lab) data.write_labs(labs) labs = data.get_labs() fill_lab_list(lab_list, labs) events = ui.get_events() events.push_layer_close_event() return remove
def toggle_lab_option(lab, option): """Toggle a boolean lab option (T/F value)""" if option in lab.options: del lab.options[option] else: lab.options[option] = "" labs = data.get_labs() data.write_labs(labs)
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 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)
def move_lab(lab_list: ui.layers.ListLayer, lab, step): """Move a lab up or down the list of labs""" labs = data.get_labs() index = labs.index(lab) # Prevent moving out of bounds if index + step > len(labs) - 1 or index + step < 0: return labs[index] = labs[index + step] labs[index + step] = lab data.write_labs(labs) lab_list.component._selected_index += step fill_lab_list(lab_list, labs)
def grade(use_locks=True, student: model.Student = None): """Create the list of labs to pick one to grade""" window = ui.get_window() labs = data.get_labs() if not labs: popup = ui.layers.Popup("Error") popup.set_message(["No labs have been created yet"]) window.run_layer(popup) return title = "Grader" if not use_locks: title = "Run for Fun" lab_list = ui.layers.ListLayer() lab_list.set_searchable("Lab") for index, lab in enumerate(labs): lab_list.add_row_text(str(lab), lab_select_fn, index, use_locks, student) window.register_layer(lab_list, title)
def add_lab(): """Add a lab to the current class""" window = ui.get_window() zy_api = Zybooks() text_input = ui.layers.TextInputLayer("Lab Name") text_input.set_prompt(["Enter the Lab Name"]) window.run_layer(text_input) if text_input.canceled: return lab_name = text_input.get_text() # Get lab part(s) parts = [] section_selector = ZybookSectionSelector(allow_optional_and_hidden=True) section_numbers = section_selector.select_zybook_sections( return_just_numbers=True) for chapter, section in section_numbers: part = {} response = zy_api.get_zybook_section(chapter, section) if not response.success: popup = ui.layers.Popup("Error", ["Invalid URL"]) window.run_layer(popup) part["name"] = response.name part["id"] = response.id parts.append(part) new_lab = data.model.Lab(lab_name, parts, {}) edit_lab_options(new_lab) all_labs = data.get_labs() all_labs.append(new_lab) data.write_labs(all_labs)