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 handle_args(args): if args.set_data_dir: if preferences.set_data_directory(args.set_data_dir): print( f"Successfully set the shared data dir to {args.set_data_dir}") time.sleep(1) else: print(f'Error: The path "{args.set_data_dir}" does not exist.') sys.exit() if args.init_data_dir: if not os.path.exists(args.init_data_dir): print(f"Error: the path {args.init_data_dir} does not exist.") sys.exit() shared_data_dir = os.path.join(args.init_data_dir, "zygrader_data") print(f"Warning: You are about to create the following directory:") print(f"{shared_data_dir}") print(f"Any existing data will be overwritten.") print(f"Type 'Y' to confirm, anything else to cancel: ", end="") confirm = input() if confirm not in {"Y", "y", "yes", "Yes"}: print("Canceled") sys.exit() SharedData.create_shared_data_directory(shared_data_dir) preferences.set_data_directory(shared_data_dir) print(f"Successfully created the shared data dir at {shared_data_dir}") print() print(f"Instruct users to run zygrader with the following flag:") print(f"zygrader --set-data-dir {shared_data_dir}") sys.exit()
def watch_students(student_list, students, lab, use_locks): """Register paths when the filtered list is created""" paths = [ SharedData.get_locks_directory(), SharedData.get_flags_directory() ] data.fs_watch.fs_watch_register(paths, "student_list_watch", fill_student_list, student_list, students, lab, use_locks, student_select_fn)
def get_submission_zip(self, url): """Download the submission at the given URL, or from a local cache if available While this is technically accessing files at Amazon's servers, it is coupled closely enough to zyBooks to include it in this file, rather than making a new file for just this feature. The cache is stored in the zygrader_data/SEMESTER_FOLDER/.cache/ directory Returns a ZipFile """ # Check if the zip file is already cached. Only use the basename of the url cached_name = os.path.join(SharedData.get_cache_directory(), os.path.basename(url)) if os.path.exists(cached_name): return zipfile.ZipFile(cached_name) # If not cached, download zip_response = requests.get(url) if not zip_response.ok: return Zybooks.ERROR # Write zip to cache with open(cached_name, "wb") as _file: _file.write(zip_response.content) return zipfile.ZipFile(cached_name)
def remove_lock_file(_file): """Remove a specific lock file (not logged to locks_log.csv)""" locks_directory = SharedData.get_locks_directory() os.remove(os.path.join(locks_directory, _file)) logger.log("lock file was removed manually", _file, logger.WARNING)
def get_flag_file_path(student: Student, lab: Lab): """Return path for a unique flag file given a student and lab""" lab_name = lab.get_unique_name() student_name = student.get_unique_name() flag_path = f"{lab_name}.{student_name}.flag" return os.path.join(SharedData.get_flags_directory(), flag_path)
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 change_class(): """Change the current class. This applies globally to all users of zygrader. """ window = ui.get_window() class_codes = SharedData.get_class_codes() popup = ui.layers.ListLayer("Class Code", popup=True) for code in class_codes: popup.add_row_text(code) window.run_layer(popup) if popup.canceled: return code_index = popup.selected_index() SharedData.set_current_class_code(class_codes[code_index]) popup = ui.layers.Popup("Changed Class", [f"Class changed to {class_codes[code_index]}"]) window.run_layer(popup)
def write_labs(labs): SharedData.LABS = labs labs_json = [] for lab in labs: labs_json.append(lab.to_json()) path = SharedData.get_labs_data() with open(path, "w") as _file: json.dump(labs_json, _file, indent=2)
def write_tas(tas): SharedData.TAS = tas tas_json = [] for ta in tas: tas_json.append(ta.to_json()) path = SharedData.get_ta_data() with open(path, "w") as _file: json.dump(tas_json, _file, indent=2)
def write_class_sections(class_sections): SharedData.CLASS_SECTIONS = class_sections class_sections_json = [] for class_section in class_sections: class_sections_json.append(class_section.to_json()) path = SharedData.get_class_sections_data() with open(path, "w") as _file: json.dump(class_sections_json, _file, indent=2)
def unlock_all_labs_by_grader(username: str): """Remove all lock files for a given grader""" # Look at all lock files for lock in get_lock_files(): lock_parts = lock.split(".") # Only look at the lock files graded by the current grader if lock_parts[0] == username: os.remove(os.path.join(SharedData.get_locks_directory(), lock)) logger.log("All locks under the current grader were removed", logger.WARNING)
def load_class_sections() -> list: SharedData.CLASS_SECTIONS.clear() path = SharedData.get_class_sections_data() if not os.path.exists(path): return [] with open(path, "r") as class_sections_file: class_sections_json = json.load(class_sections_file) for class_section in class_sections_json: SharedData.CLASS_SECTIONS.append(ClassSection.from_json(class_section)) return SharedData.CLASS_SECTIONS
def load_labs() -> list: SharedData.LABS.clear() path = SharedData.get_labs_data() if not os.path.exists(path): return [] with open(path, "r") as labs_file: labs_json = json.load(labs_file) for a in labs_json: SharedData.LABS.append(Lab(a["name"], a["parts"], a["options"])) return SharedData.LABS
def load_tas() -> list: SharedData.TAS.clear() path = SharedData.get_ta_data() if not os.path.exists(path): return [] with open(path, "r") as tas_file: tas_json = json.load(tas_file) for ta in tas_json: SharedData.TAS.append(TA.from_json(ta)) return SharedData.TAS
def get_lock_file_path(student: Student, lab: Lab = None): """Return path for lock file""" username = getpass.getuser() # We can safely store both lab+student and lab locks in the # Same directory if lab: lab_name = lab.get_unique_name() student_name = student.get_unique_name() lock_path = f"{username}.{lab_name}.{student_name}.lock" else: student_name = student.get_unique_name() lock_path = f"{username}.{student_name}.lock" return os.path.join(SharedData.get_locks_directory(), lock_path)
def load_students() -> list: SharedData.STUDENTS.clear() path = SharedData.get_student_data() if not os.path.exists(path): return [] with open(path, "r") as students_file: try: students_json = json.load(students_file) except json.decoder.JSONDecodeError: students_json = [] for student in students_json: SharedData.STUDENTS.append( Student( student["first_name"], student["last_name"], student["email"], student["section"], student["id"], )) return SharedData.STUDENTS
def save_roster(roster): """Save the roster of students to a json file""" roster = roster["roster"] # It is stored under "roster" in the json # Download students (and others) students = [] for role in roster: for person in roster[role]: student = {} student["first_name"] = person["first_name"] student["last_name"] = person["last_name"] student["email"] = person["primary_email"] student["id"] = person["user_id"] if "class_section" in person: student["section"] = person["class_section"]["value"] else: student["section"] = -1 students.append(student) out_path = SharedData.get_student_data() with open(out_path, "w") as _file: json.dump(students, _file, indent=2)
def get_flag_files(): """Return a list of all flag files""" return [f for f in os.listdir(SharedData.get_flags_directory())]
def get_lock_log_path(): """Return path to lock log file""" return os.path.join(SharedData.get_logs_directory(), "locks_log.csv")
def unlock_all_labs(): """Remove all locks""" for lock in get_lock_files(): os.remove(os.path.join(SharedData.get_locks_directory(), lock))
def get_lock_files(): """Return a list of all lock files""" return [ l for l in os.listdir(SharedData.get_locks_directory()) if l.endswith(".lock") ]
def get_global_lock_path(): return os.path.join(SharedData.get_logs_directory(), "log.txt")
def lock_cleanup(): # If terminating before shared directories are initialized, the folders would be # created in the current directory when removing locks. See #72 for more details. if SharedData.is_initialized(): data.lock.unlock_all_labs_by_grader(getpass.getuser())