Exemple #1
0
def score_from_csv(assign_id, rows, kind='total', invalid=None, message=None):
    """
    Job for uploading Scores.

    @param ``rows`` should be a list of records (mappings),
        with labels `email` and `score`
    """
    log = jobs.get_job_logger()
    current_user = jobs.get_current_job().user
    assign = Assignment.query.get(assign_id)

    message = message or '{} score for {}'.format(kind.title(), assign.display_name)

    def log_err(msg):
        log.info('\t!  {}'.format(msg))

    log.info("Uploading scores for {}:\n".format(assign.display_name))

    if invalid:
        log_err('skipping {} invalid entries on lines:'.format(len(invalid)))
        for line in invalid:
            log_err('\t{}'.format(line))
        log.info('')

    success, total = 0, len(rows)
    for i, row in enumerate(rows, start=1):
        try:
            email, score = row['email'], row['score']
            user = User.query.filter_by(email=email).one()

            backup = Backup.query.filter_by(assignment=assign, submitter=user, submit=True).first()
            if not backup:
                backup = Backup.create(submitter=user, assignment=assign, submit=True)

            uploaded_score = Score(grader=current_user, assignment=assign,
                    backup=backup, user=user, score=score, kind=kind, message=message)

            db.session.add(uploaded_score)
            uploaded_score.archive_duplicates()

        except SQLAlchemyError:
            print_exc()
            log_err('error: user with email `{}` does not exist'.format(email))
        else:
            success += 1
        if i % 100 == 0:
            log.info('\nUploaded {}/{} Scores\n'.format(i, total))
    db.session.commit()

    log.info('\nSuccessfully uploaded {} "{}" scores (with {} errors)'.format(success, kind, total - success))

    return '/admin/course/{cid}/assignments/{aid}/scores'.format(
                cid=jobs.get_current_job().course_id, aid=assign_id)
Exemple #2
0
def assign_scores(assign_id, score, kind, message, deadline,
                     include_backups=True):
    logger = jobs.get_job_logger()
    current_user = jobs.get_current_job().user

    assignment = Assignment.query.get(assign_id)
    students = [e.user_id for e in assignment.course.get_students()]
    submission_time = server_time_obj(deadline, assignment.course)

    # Find all submissions (or backups) before the deadline
    backups = Backup.query.filter(
        Backup.assignment_id == assign_id,
        or_(Backup.created <= deadline, Backup.custom_submission_time <= deadline)
    ).order_by(Backup.created.desc()).group_by(Backup.submitter_id)

    if not include_backups:
        backups = backups.filter(Backup.submit == True)

    all_backups =  backups.all()

    if not all_backups:
        logger.info("No submissions were found with a deadline of {}."
                    .format(deadline))
        return "No Scores Created"

    total_count = len(all_backups)
    logger.info("Found {} eligible submissions...".format(total_count))

    score_counter, seen = 0, set()

    for back in all_backups:
        if back.creator in seen:
            score_counter += 1
            continue
        new_score = Score(score=score, kind=kind, message=message,
                          user_id=back.submitter_id,
                          assignment=assignment, backup=back,
                          grader=current_user)
        db.session.add(new_score)
        new_score.archive_duplicates()
        db.session.commit()

        score_counter += 1
        if score_counter % 5 == 0:
            logger.info("Scored {} of {}".format(score_counter, total_count))
        seen |= back.owners()

    result = "Left {} '{}' scores of {}".format(score_counter, kind.title(), score)
    logger.info(result)
    return result
Exemple #3
0
    def test_scores_with_generate(self, generate=False):
        if generate:
            db.drop_all()
            db.create_all()
            generate.seed()
            self.login('*****@*****.**')
        else:
            backup = Backup.query.filter_by(submitter_id=self.user1.id,
                                            submit=True).first()
            score = Score(backup_id=backup.id,
                          kind="Composition",
                          score=2.0,
                          message="Good work",
                          assignment_id=self.assignment.id,
                          user_id=backup.submitter_id,
                          grader=self.staff1)
            db.session.add(score)
            db.session.commit()
            self.login(self.staff1.email)

        endpoint = '/admin/course/1/assignments/1/scores.csv'
        response = self.client.get(endpoint)
        self.assert_200(response)
        csv_rows = list(csv.reader(StringIO(str(response.data, 'utf-8'))))

        scores = Score.query.filter_by(assignment_id=1).all()

        backup_creators = []
        for s in scores:
            backup_creators.extend(s.backup.owners())

        self.assertEquals(len(backup_creators), len(csv_rows) - 1)
Exemple #4
0
 def add_score(backup, kind, score, archived=False):
     score = Score(
         created=backup.created,
         backup_id=backup.id,
         assignment_id=backup.assignment_id,
         kind=kind,
         score=score,
         message='Good work',
         user_id=backup.submitter_id,
         grader_id=self.staff1.id,
         archived=archived,
     )
     db.session.add(score)
Exemple #5
0
def gen_score(backup, admin, kind="autograder"):
    created = datetime.datetime.now() - datetime.timedelta(
        minutes=random.randrange(100))
    if kind == "composition":
        score = random.randrange(2)
    else:
        score = random.uniform(0, 100)

    return Score(created=created,
                 backup_id=backup.id,
                 assignment_id=backup.assignment.id,
                 grader_id=admin.id,
                 kind=kind,
                 score=score,
                 message=loremipsum.get_sentence())
Exemple #6
0
    def new(data: Dict[str, Any], token: str, id_: str, **kwargs: Any) -> None:
        score = data['score']

        with managed_session() as session:
            s = Score(score=score, user_id=id_)
            session.add(s)

            kwargs['client_send']({
                'headers': {
                    'path': 'scores/post_new',
                    'status': Status.SUCCESS.value
                }
            })

            user = session.query(User).filter(User.id == id_).first()
            logger.info(f'User "{user.username}" sent a new score of {score}')
Exemple #7
0
def grade(bid):
    """ Used as a form submission endpoint. """
    backup = Backup.query.options(db.joinedload('assignment')).get(bid)
    if not backup:
        abort(404)
    if not Backup.can(backup, current_user, 'grade'):
        flash("You do not have permission to score this assignment.", "warning")
        abort(401)

    form = forms.GradeForm()
    score_kind = form.kind.data.strip().lower()
    is_composition = (score_kind == "composition")
    # TODO: Form should include redirect url instead of guessing based off tag

    if is_composition:
        form = forms.CompositionScoreForm()

    if not form.validate_on_submit():
        return grading_view(backup, form=form)

    score = Score(backup=backup, grader=current_user,
                  assignment_id=backup.assignment_id)
    form.populate_obj(score)
    db.session.add(score)
    db.session.commit()

    # Archive old scores of the same kind
    score.archive_duplicates()

    next_page = None
    flash_msg = "Added a {0} {1} score.".format(score.score, score_kind)

    # Find GradingTasks applicable to this score
    tasks = backup.grading_tasks
    for task in tasks:
        task.score = score
        cache.delete_memoized(User.num_grading_tasks, task.grader)

    db.session.commit()

    if len(tasks) == 1:
        # Go to next task for the current task queue if possible.
        task = tasks[0]
        next_task = task.get_next_task()
        next_route = '.composition' if is_composition else '.grading'
        # Handle case when the task is on the users queue
        if next_task:
            flash_msg += (" There are {0} tasks left. Here's the next submission:"
                          .format(task.remaining))
            next_page = url_for(next_route, bid=next_task.backup_id)
        else:
            flash_msg += " All done with grading for {}".format(backup.assignment.name)
            next_page = url_for('.grading_tasks')
    else:
        # TODO: Send task id or redirect_url in the grading form
        # For now, default to grading tasks
        next_page = url_for('.grading_tasks')

    flash(flash_msg, 'success')

    if not next_page:
        next_page = url_for('.assignment_queues', aid=backup.assignment_id,
                            cid=backup.assignment.course_id)
    return redirect(next_page)
Exemple #8
0
    def test_publish_grades(self):
        scores, users = {}, [self.user1, self.user3]
        for score_kind in ['total', 'composition']:
            for user in users:
                for assign in self.active_assignments:
                    backup = assign.final_submission(
                        assign.active_user_ids(user.id))
                    duplicate_score = False
                    for s in backup.scores:
                        if s.kind == score_kind:
                            duplicate_score = True
                    if not duplicate_score:
                        if score_kind == "composition":
                            point = random.randrange(2)
                        else:
                            point = random.uniform(0, 100)
                    scores = Score(backup_id=backup.id,
                                   kind=score_kind,
                                   score=point,
                                   message="Good work",
                                   assignment_id=assign.id,
                                   user_id=backup.submitter_id,
                                   grader=self.staff1)
                    db.session.add(scores)
        db.session.commit()

        def publish_scores(assignment, kinds_to_publish):
            endpoint = '/admin/course/{}/assignments/{}/publish'.format(
                assignment.course.id, assignment.id)
            data = werkzeug.datastructures.MultiDict(
                ('published_scores', kind) for kind in SCORE_KINDS
                if kind.title() in kinds_to_publish)
            data['csrf_token'] = 'token'  # need at least one form field?
            response = self.client.post(endpoint,
                                        data=data,
                                        follow_redirects=True)
            self.assert_200(response)
            return response

        def check_visible_scores(user, assignment, hidden=(), visible=()):
            self.login(user.email)
            endpoint = '/{}/'.format(assignment.name)
            r = self.client.get(endpoint)
            self.assert_200(r)
            s = r.get_data().decode("utf-8")
            for score in hidden:
                self.assertFalse("{}:".format(score) in s)
            for score in visible:
                self.assertTrue("{}:".format(score) in s)

        # Checks that by default scores are hidden
        for user, assign in zip(users, self.active_assignments):
            check_visible_scores(user, assign, hidden=['Total', 'Composition'])

        # Lab assistants and students cannot make changes
        for email in [self.lab_assistant1.email, self.user1.email]:
            self.login(email)
            response = publish_scores(self.assignment, [])
            source = response.get_data().decode('utf-8')
            self.assertIn('You are not on the course staff', source)

        # Adding total tag by staff changes score visibility for all users for that assignment
        self.login(self.staff1.email)
        response = publish_scores(self.assignment, ['Total'])
        source = response.get_data().decode('utf-8')
        self.assertTrue("Saved published scores for {}".format(
            self.assignment.display_name) in source)
        for user in users:
            check_visible_scores(user,
                                 self.assignment,
                                 hidden=['Composition'],
                                 visible=['Total'])
            check_visible_scores(user,
                                 self.assignment2,
                                 hidden=['Total', 'Composition'])

        # Admin can publish and hide scores
        self.login('*****@*****.**')
        response = publish_scores(self.assignment2, ['Composition'])
        for user in users:
            check_visible_scores(user,
                                 self.assignment,
                                 hidden=['Composition'],
                                 visible=['Total'])
            check_visible_scores(user,
                                 self.assignment2,
                                 hidden=['Total'],
                                 visible=['Composition'])

        # Hiding score only affect targeted assignment
        self.login(self.staff1.email)
        response = publish_scores(self.assignment, [])
        source = response.get_data().decode('utf-8')
        self.assertTrue("Saved published scores for {}".format(
            self.assignment.display_name) in source)
        for user in users:
            check_visible_scores(user,
                                 self.assignment,
                                 hidden=['Total', 'Composition'])
            check_visible_scores(user,
                                 self.assignment2,
                                 hidden=['Total'],
                                 visible=['Composition'])

        self.login(self.staff1.email)
        endpoint = '/admin/course/{}/assignments/{}/publish'.format(
            self.course.id, self.assignment2.id)
        response = publish_scores(self.assignment2, ['Composition', 'Total'])
        for user in users:
            check_visible_scores(user,
                                 self.assignment,
                                 hidden=['Total', 'Composition'])
            check_visible_scores(user,
                                 self.assignment2,
                                 visible=['Composition', 'Total'])

        # If assignment is not visible, still can publish
        self.assignment.visible = False
        self.login(self.staff1.email)
        response = publish_scores(self.assignment, ['Total'])
        source = response.get_data().decode('utf-8')
        self.assertTrue("Saved published scores for {}".format(
            self.assignment.display_name) in source)
        for user in users:
            check_visible_scores(user,
                                 self.assignment,
                                 visible=['Total'],
                                 hidden=['Composition'])
            check_visible_scores(user,
                                 self.assignment2,
                                 visible=['Composition', 'Total'])
Exemple #9
0
def grade(bid):
    """ Used as a form submission endpoint. """
    backup = Backup.query.options(db.joinedload('assignment')).get(bid)
    if not backup:
        abort(404)
    if not Backup.can(backup, current_user, 'grade'):
        flash("You do not have permission to score this assignment.", "warning")
        abort(401)

    form = forms.GradeForm()
    score_kind = form.kind.data.strip().lower()
    is_composition = (score_kind == "composition")
    # TODO: Form should include redirect url instead of guessing based off tag

    if is_composition:
        form = forms.CompositionScoreForm()

    if not form.validate_on_submit():
        return grading_view(backup, form=form)

    score = Score(backup=backup, grader=current_user,
                  assignment_id=backup.assignment_id)
    form.populate_obj(score)
    db.session.add(score)
    db.session.commit()

    # Archive old scores of the same kind
    score.archive_duplicates()

    next_page = None
    flash_msg = "Added a {0} {1} score.".format(score.score, score_kind)

    # Find GradingTasks applicable to this score
    tasks = backup.grading_tasks
    for task in tasks:
        task.score = score
        cache.delete_memoized(User.num_grading_tasks, task.grader)

    db.session.commit()

    if len(tasks) == 1:
        # Go to next task for the current task queue if possible.
        task = tasks[0]
        next_task = task.get_next_task()
        next_route = '.composition' if is_composition else '.grading'
        # Handle case when the task is on the users queue
        if next_task:
            flash_msg += (" There are {0} tasks left. Here's the next submission:"
                          .format(task.remaining))
            next_page = url_for(next_route, bid=next_task.backup_id)
        else:
            flash_msg += " All done with grading for {}".format(backup.assignment.name)
            next_page = url_for('.grading_tasks')
    else:
        # TODO: Send task id or redirect_url in the grading form
        # For now, default to grading tasks
        next_page = url_for('.grading_tasks')

    flash(flash_msg, 'success')

    if not next_page:
        next_page = url_for('.assignment_queues', aid=backup.assignment_id,
                            cid=backup.assignment.course_id)
    return redirect(next_page)
Exemple #10
0
def grade_on_effort(assignment_id, full_credit, late_multiplier, required_questions, grading_url):
    logger = jobs.get_job_logger()

    current_user = jobs.get_current_job().user
    assignment = Assignment.query.get(assignment_id)
    submissions = assignment.course_submissions(include_empty=False)

    # archive all previous effort scores for this assignment
    scores = Score.query.filter(
        Score.kind == 'effort',
        Score.assignment_id == assignment_id).all()
    for score in scores:
        db.session.delete(score)

    seen = set()
    stats = Counter()
    manual, late, not_perfect = [], [], []
    for i, subm in enumerate(submissions, 1):
        user_id = int(subm['user']['id'])
        if user_id in seen:
            continue

        latest_backup = Backup.query.get(subm['backup']['id'])
        submission_time = get_submission_time(latest_backup, assignment)
        backup, submission_time = find_best_scoring(latest_backup,
                submission_time, assignment, required_questions, full_credit)

        try:
            score, messages = effort_score(backup, full_credit, required_questions)
        except AssertionError:
            manual.append(backup)
            continue
        else:
            score, messages = handle_late(backup, assignment,
                    late, submission_time, score, messages, late_multiplier)

        if score < full_credit and backup.hashid not in late:
            not_perfect.append(backup)

        messages.append('\nFinal Score: {}'.format(score))
        messages.append('Your final score will be the max of either this score or the `Total` score (if exists)')
        new_score = Score(score=score, kind='effort',
                message='\n'.join(messages), user_id=backup.submitter_id,
                assignment=assignment, backup=backup, grader=current_user)
        db.session.add(new_score)

        if i % 100 == 0:
            logger.info('Scored {}/{}'.format(i, len(submissions)))

        if subm['group']:
            member_ids = {int(id) for id in subm['group']['group_member'].split(',')}
            seen |= member_ids
            stats[score] += len(member_ids)
        else:
            seen.add(user_id)
            stats[score] += 1

    # Commit all scores at once
    db.session.commit()

    logger.info('Scored {}/{}'.format(i, len(submissions)))
    logger.info('done!')

    if len(late) > 0:
        logger.info('\n{} Late:'.format(len(late)))
        for backup_id in late:
            logger.info('  {}'.format(grading_url + backup_id))

    logger.info('\nScore Distribution:')
    sorted_scores = sorted(stats.items(), key=lambda p: -p[0])
    for score, count in sorted_scores:
        logger.info('  {} - {}'.format(str(score).rjust(3), count))

    needs_autograding = len(manual) + len(not_perfect)
    if needs_autograding > 0:
        logger.info('\nAutograding {} manual and/or not perfect backups'.format(needs_autograding))
        backup_ids = [backup.id for backup in manual + not_perfect]
        try:
            autograde_backups(assignment, current_user.id, backup_ids, logger)
        except ValueError:
            logger.info('Could not autograde backups - Please add an autograding key.')

    db.session.commit()
    return '/admin/course/{cid}/assignments/{aid}/scores'.format(
                cid=jobs.get_current_job().course_id, aid=assignment_id)
Exemple #11
0
def assign_scores(assign_id,
                  score,
                  kind,
                  message,
                  deadline,
                  include_backups=True,
                  grade_backups=False):
    logger = jobs.get_job_logger()
    current_user = jobs.get_current_job().user

    assignment = Assignment.query.get(assign_id)
    students = [e.user_id for e in assignment.course.get_students()]
    submission_time = server_time_obj(deadline, assignment.course)

    # Find all submissions (or backups) before the deadline
    backups = Backup.query.filter(
        Backup.assignment_id == assign_id,
        or_(Backup.created <= deadline,
            Backup.custom_submission_time <= deadline)).group_by(
                Backup.submitter_id).order_by(Backup.created.desc())

    if not include_backups:
        backups = backups.filter(Backup.submit == True)

    all_backups = backups.all()

    if not all_backups:
        logger.info("No submissions were found with a deadline of {}.".format(
            deadline))
        return "No Scores Created"

    score_counter, seen = 0, set()

    unique_backups = []

    for back in all_backups:
        if back.creator not in seen:
            unique_backups.append(back)
            seen |= back.owners()

    total_count = len(unique_backups)
    logger.info(
        "Found {} unique and eligible submissions...".format(total_count))

    if grade_backups:
        logger.info('\nAutograding {} backups'.format(total_count))
        backup_ids = [back.id for back in unique_backups]
        try:
            autograde_backups(assignment, current_user.id, backup_ids, logger)
        except ValueError:
            logger.info(
                'Could not autograde backups - Please add an autograding key.')
    else:
        for back in unique_backups:
            new_score = Score(score=score,
                              kind=kind,
                              message=message,
                              user_id=back.submitter_id,
                              assignment=assignment,
                              backup=back,
                              grader=current_user)

            db.session.add(new_score)
            new_score.archive_duplicates()

            score_counter += 1
            if score_counter % 100 == 0:
                logger.info("Scored {} of {}".format(score_counter,
                                                     total_count))

        # only commit if all scores were successfully added
        db.session.commit()

    logger.info("Left {} '{}' scores of {}".format(score_counter, kind.title(),
                                                   score))
    return '/admin/course/{cid}/assignments/{aid}/scores'.format(
        cid=jobs.get_current_job().course_id, aid=assignment.id)
Exemple #12
0
def assign_scores(assign_id, score, kind, message, deadline,
                     include_backups=True, grade_backups=False):
    logger = jobs.get_job_logger()
    current_user = jobs.get_current_job().user

    assignment = Assignment.query.get(assign_id)
    students = [e.user_id for e in assignment.course.get_students()]
    submission_time = server_time_obj(deadline, assignment.course)

    # Find all submissions (or backups) before the deadline
    backups = Backup.query.filter(
        Backup.assignment_id == assign_id,
        or_(Backup.created <= deadline, Backup.custom_submission_time <= deadline)
    ).group_by(Backup.submitter_id).order_by(Backup.created.desc())

    if not include_backups:
        backups = backups.filter(Backup.submit == True)

    all_backups =  backups.all()

    if not all_backups:
        logger.info("No submissions were found with a deadline of {}."
                    .format(deadline))
        return "No Scores Created"

    score_counter, seen = 0, set()

    unique_backups = []

    for back in all_backups:
        if back.creator not in seen:
            unique_backups.append(back)
            seen |= back.owners()

    total_count = len(unique_backups)
    logger.info("Found {} unique and eligible submissions...".format(total_count))

    if grade_backups:
        logger.info('\nAutograding {} backups'.format(total_count))
        backup_ids = [back.id for back in unique_backups]
        try:
            autograde_backups(assignment, current_user.id, backup_ids, logger)
        except ValueError:
            logger.info('Could not autograde backups - Please add an autograding key.')
    else:
        for back in unique_backups:
            new_score = Score(score=score, kind=kind, message=message,
                              user_id=back.submitter_id,
                              assignment=assignment, backup=back,
                              grader=current_user)

            db.session.add(new_score)
            new_score.archive_duplicates()

            score_counter += 1
            if score_counter % 100 == 0:
                logger.info("Scored {} of {}".format(score_counter, total_count))

        # only commit if all scores were successfully added
        db.session.commit()

    logger.info("Left {} '{}' scores of {}".format(score_counter, kind.title(), score))
    return '/admin/course/{cid}/assignments/{aid}/scores'.format(
                cid=jobs.get_current_job().course_id, aid=assignment.id)