Ejemplo n.º 1
0
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")
Ejemplo n.º 2
0
    def update_preferences(self):
        theme_key = preferences.get("theme")
        new_theme = themes.THEMES[theme_key]
        if new_theme is not self.theme:
            new_theme.adjust_screen_colors()
        self.theme = new_theme
        self.unicode_mode = preferences.get("unicode_mode")
        self.clear_filter = preferences.get("clear_filter")

        self.update_window()
Ejemplo n.º 3
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
Ejemplo n.º 4
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)
Ejemplo n.º 5
0
 def select_output_file(self):
     default_output_path = os.path.join(preferences.get("output_dir"),
                                        "shaken-stats.csv")
     path = filename_input(purpose="the shaken stats",
                           text=default_output_path)
     if path is None:
         return False
     self.output_path = path
     return True
Ejemplo n.º 6
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")
Ejemplo n.º 7
0
def main(window: ui.Window, args):
    """Curses has been initialized, now setup various modules before showing the menu"""
    # Register preference update callback
    preferences.add_observer(preference_update_fn)
    preferences.update_observers()

    # Notify the user of changes
    versioning.show_versioning_message(window)

    # Start file watch thread
    data.fs_watch.start_fs_watch()

    # Authenticate the user
    zybooks_api = zybooks.Zybooks()
    if not preferences.get("refresh_token") or not user.authenticate(
            window, zybooks_api):
        user.login(window)

    logger.log("zygrader started")

    mainloop(args)
Ejemplo n.º 8
0
def view_diff(first: model.Submission, second: model.Submission):
    """View a diff of the two submissions"""
    if (first.flag & model.SubmissionFlag.NO_SUBMISSION
            or second.flag & model.SubmissionFlag.NO_SUBMISSION):
        window = ui.get_window()
        popup = ui.layers.Popup("No Submissions", [
            "Cannot diff submissions because at least one student has not submitted."
        ])
        window.run_layer(popup)
        return

    use_browser = preferences.get("browser_diff")

    paths_a = utils.get_source_file_paths(first.files_directory)
    paths_b = utils.get_source_file_paths(second.files_directory)

    paths_a.sort()
    paths_b.sort()

    diff = utils.make_diff_string(paths_a, paths_b, first.student.full_name,
                                  second.student.full_name, use_browser)
    utils.view_string(diff, "submissions.diff", use_browser)
Ejemplo n.º 9
0
def attendance_score():
    """Calculate the participation score from the attendance score columns"""
    window = ui.get_window()

    if not _confirm_gradebook_ready():
        return

    # Make use of many functions from gradepuller to avoid code duplication
    puller = grade_puller.GradePuller()
    try:
        puller.read_canvas_csv()

        popup = ui.layers.Popup("Selection")
        popup.set_message(["First Select the Participation Score Assignment"])
        window.run_layer(popup)
        participation_score_assignment = puller.select_canvas_assignment()

        popup.set_message(["Next Select the first Classes Missed Assignment"])
        window.run_layer(popup)
        start_classes_missed_assignment = puller.select_canvas_assignment()

        popup.set_message(["Next Select the last Classes Missed Assignment"])
        window.run_layer(popup)
        end_classes_missed_assignment = puller.select_canvas_assignment()

        class_sections = puller.select_class_sections()

    except grade_puller.GradePuller.StoppingException:
        return

    # Get all of the assignments between the start and end
    start_index = puller.canvas_header.index(start_classes_missed_assignment)
    end_index = puller.canvas_header.index(end_classes_missed_assignment)
    all_classes_missed_assignments = puller.canvas_header[
        start_index:end_index + 1]

    # Figure out the grading scheme - the mapping from classes missed to grade
    builtin_schemes = [
        ("TR", [100, 100, 98, 95, 91, 86, 80, 73, 65, 57, 49, 46]),
        ("MWF", [100, 100, 99, 97, 94, 90, 85, 80, 75, 70, 65, 60, 55, 53]),
    ]
    scheme_selector = ui.layers.ListLayer("Scheme Selector", popup=True)
    for name, scheme in builtin_schemes:
        scheme_selector.add_row_text(
            f"{name}: {','.join(map(str,scheme))},...")
    scheme_selector.add_row_text("Create New Scheme")

    window.run_layer(scheme_selector)
    if scheme_selector.canceled:
        return

    selected = scheme_selector.selected_index()
    if selected < len(builtin_schemes):
        points_by_classes_missed = builtin_schemes[selected][1]
    else:
        # Get the custom scheme
        scheme_inputter = ui.layers.TextInputLayer("New Scheme")
        scheme_inputter.set_prompt([
            "Enter a new scheme as a comma-separated list",
            "e.g. '100,100,95,90,85,80,78'",
            "",
            "The difference between the last two values will be repeated"
            " until a score of 0 is reached",
        ])
        window.run_layer(scheme_inputter)
        if scheme_inputter.canceled:
            return
        scheme_text = scheme_inputter.get_text()
        points_by_classes_missed = list(map(int, scheme_text.split(',')))

    # Extend the scheme until 0 is reached
    delta = points_by_classes_missed[-2] - points_by_classes_missed[-1]
    while points_by_classes_missed[-1] >= 0:
        points_by_classes_missed.append(points_by_classes_missed[-1] - delta)
    # Get rid of the negative element
    del points_by_classes_missed[-1]

    # Calculate and assign the grade for each student
    for student in puller.canvas_students.values():
        if student["section_number"] in class_sections:
            total_classes_missed = 0
            for assignment in all_classes_missed_assignments:
                try:
                    total_classes_missed += int(student[assignment])
                except ValueError:
                    total_classes_missed += int(
                        puller.canvas_points_out_of[assignment])

            try:
                grade = points_by_classes_missed[total_classes_missed]
            except IndexError:
                grade = 0
            student[participation_score_assignment] = grade

    out_path = filename_input(purpose="the partipation score",
                              text=os.path.join(preferences.get("output_dir"),
                                                "participation.csv"))
    if out_path is None:
        return

    # Again use the gradepuller functionality
    # We just need to programmatically set the selected assignments
    puller.selected_assignments = [participation_score_assignment]
    # And the involved class sections
    puller.involved_class_sections = set(class_sections)
    puller.write_upload_file(out_path, restrict_sections=True)
Ejemplo n.º 10
0
def midterm_mercy():
    """Replace the lower of the two midterm scores with the final exam score"""
    window = ui.get_window()

    if not _confirm_gradebook_ready():
        return

    # Use the Canvas parsing from the gradepuller to get the gradebook in
    # also use the selection of canvas assignments from the gradepuller
    puller = grade_puller.GradePuller()
    try:
        puller.read_canvas_csv()

        popup = ui.layers.Popup("Selection")
        popup.set_message(["First Select the Midterm 1 Assignment"])
        window.run_layer(popup)
        midterm_1_assignment = puller.select_canvas_assignment()

        midterm_2_assignment = None
        double_midterm_popup = ui.layers.BoolPopup("2 Midterms")
        double_midterm_popup.set_message([
            "Is there a second midterm this semester?",
            "If so, select that assignment next."
        ])
        window.run_layer(double_midterm_popup)
        if double_midterm_popup.canceled:
            return
        if double_midterm_popup.get_result():
            midterm_2_assignment = puller.select_canvas_assignment()

        popup.set_message(["Next Select the Final Exam Assignment"])
        window.run_layer(popup)
        final_exam_assignment = puller.select_canvas_assignment()

    except grade_puller.GradePuller.StoppingException:
        return

    # Do the replacement for each student
    for student in puller.canvas_students.values():
        midterm_1_score = float(student[midterm_1_assignment])
        midterm_2_score = (float(student[midterm_2_assignment])
                           if midterm_2_assignment else None)
        final_exam_score = float(student[final_exam_assignment])

        if midterm_2_assignment:
            # Figure out lower midterm, then if it should be replaced do so
            if midterm_2_score < midterm_1_score:
                if final_exam_score > midterm_2_score:
                    student[midterm_2_assignment] = final_exam_score
            else:
                if final_exam_score > midterm_1_score:
                    student[midterm_1_assignment] = final_exam_score
        else:
            # With only one midterm, just replace it if lower than final
            if final_exam_score > midterm_1_score:
                student[midterm_1_assignment] = final_exam_score

    out_path = filename_input(purpose="the updated midterm scores",
                              text=os.path.join(preferences.get("output_dir"),
                                                "midterm_mercy.csv"))
    if out_path is None:
        return

    # Again use the gradepuller functionality
    # We just need to programmatically set the selected assignments
    puller.selected_assignments = [midterm_1_assignment]
    if midterm_2_assignment:
        puller.selected_assignments.append(midterm_2_assignment)
    puller.write_upload_file(out_path)

    popup = ui.layers.Popup("Reminder")
    popup.set_message([
        "Don't forget to manually correct as necessary"
        " (for any students who should not have a score replaced)."
    ])
    window.run_layer(popup)
Ejemplo n.º 11
0
 def update_preferences(self):
     """Update the input settings from user preferences"""
     self.vim_mode = preferences.get("vim_mode")
     self.left_right_menu_nav = preferences.get("left_right_arrow_nav")
     self.use_esc_back = preferences.get("use_esc_back")
Ejemplo n.º 12
0
 def __load_session(self):
     Zybooks.refresh_token = preferences.get("refresh_token")