예제 #1
0
    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
예제 #2
0
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())
예제 #3
0
 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
예제 #4
0
    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
예제 #5
0
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)
예제 #6
0
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")
예제 #7
0
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")
예제 #8
0
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)
예제 #9
0
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()
예제 #10
0
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()
예제 #11
0
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)
예제 #12
0
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
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
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)
예제 #16
0
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)
예제 #17
0
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
예제 #18
0
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)
예제 #19
0
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")
예제 #20
0
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")
예제 #21
0
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()
예제 #22
0
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)
예제 #23
0
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()
예제 #24
0
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")
예제 #25
0
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")
예제 #26
0
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)
예제 #27
0
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)
예제 #28
0
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()
예제 #29
0
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")
예제 #30
0
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)