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)
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
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)
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)
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())
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}')
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)
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'])
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)
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)
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)
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)