def validate_team(db, team_id, round_id, now, with_member=None, without_member=None): """ Raise an exception if the team is invalid for the round. If with_member is a user, check with the user added the team. """ team_members = db.tables.team_members tm_query = db.query(team_members) \ .where(team_members.team_id == team_id) n_members = db.count(tm_query.fields(team_members.user_id)) n_qualified = db.count( tm_query.where(team_members.is_qualified).fields(team_members.user_id)) if with_member is not None: n_members += 1 if with_member['is_qualified']: n_qualified += 1 if without_member is not None: n_members -= 1 if with_member['is_qualified']: n_qualified -= 1 round_ = load_round(db, round_id, now=now) if n_members < round_['min_team_size']: raise ModelError('team too small') if n_members > round_['max_team_size']: raise ModelError('team too large') if n_qualified < n_members * round_['min_team_ratio']: raise ModelError('not enough qualified members')
def validate_team( db, team_id, round_id, now, with_member=None, without_member=None): """ Raise an exception if the team is invalid for the round. If with_member is a user, check with the user added the team. """ team_members = db.tables.team_members tm_query = db.query(team_members) \ .where(team_members.team_id == team_id) n_members = db.count( tm_query.fields(team_members.user_id)) n_qualified = db.count( tm_query.where(team_members.is_qualified) .fields(team_members.user_id)) if with_member is not None: n_members += 1 if with_member['is_qualified']: n_qualified += 1 if without_member is not None: n_members -= 1 if with_member['is_qualified']: n_qualified -= 1 round_ = load_round(db, round_id, now=now) if n_members < round_['min_team_size']: raise ModelError('team too small') if n_members > round_['max_team_size']: raise ModelError('team too large') if n_qualified < n_members * round_['min_team_ratio']: raise ModelError('not enough qualified members')
def submit_user_attempt_answer_action(request): now = datetime.utcnow() attempt_id = request.context.attempt_id attempt = load_attempt(request.db, attempt_id) participation = load_participation(request.db, attempt['participation_id']) round_ = load_round(request.db, participation['round_id']) if round_['status'] != 'open': return {'success': False, 'error': 'round not open'} if participation['started_at'] is not None and round_[ 'duration'] is not None: duration = timedelta(minutes=round_['duration']) deadline = participation['started_at'] + duration if now > deadline: return {'success': False, 'error': 'past deadline'} submitter_id = request.context.user_id query = request.json_body answer = query['answer'] revision_id = None if 'data' in query: revision = query['data'] revision_id = store_revision_query(request.db, submitter_id, attempt_id, revision) answer, feedback = grade_answer(request.db, attempt_id, submitter_id, revision_id, answer, now=now) return { 'success': True, 'answer_id': answer['id'], 'revision_id': revision_id, 'feedback': feedback, 'score': answer['score'] }
def create_team_action(request): """ Create a team for the context's user. An administrator can also perform the action on a user's behalf. """ # Create the team. now = datetime.utcnow() user_id = request.context.user_id user = load_user(request.db, user_id) # Select a round based on the user's badges. round_ids = find_round_ids_with_badges(request.db, user['badges'], now) if len(round_ids) == 0: # The user does not have access to any open round. raise ApiError('not qualified for any open round') if len(round_ids) > 1: # XXX The case where a user has badges for multiple open rounds # is currently handled by picking the first one, which is the # one that has the greatest id. This is unsatisfactory. pass round_id = round_ids[0] round_ = load_round(request.db, round_id, now) if not round_['is_registration_open']: raise ApiError('registration is closed') # Create the team. team_id = create_user_team(request.db, user_id, now) # Create a participation. create_participation(request.db, team_id, round_id, now=now) # Ensure the user gets team credentials. reset_user_principals(request) return {'success': True}
def leave_team(db, user_id, team_id, now): """ Remove a user from their team. """ user = load_user(db, user_id, for_update=True) team_id = user['team_id'] # The user must be member of a team. if team_id is None: raise ModelError('no team') team = load_team(db, team_id) # Load the participation. participation_id = get_team_latest_participation_id(db, team_id) participation = load_participation(db, participation_id) round_id = participation['round_id'] round_ = load_round(db, round_id) # If the team already has an attempt (is_locked=True), verify # that the team remains valid if the user is removed. if team['is_locked']: if round_['allow_team_changes']: validate_team(db, team_id, round_id, without_member=user, now=now) else: raise ModelError('team is locked') # Clear the user's team_id. set_user_team_id(db, user_id, None) # Delete the team_members row. team_members = db.tables.team_members tm_query = db.query(team_members) \ .where(team_members.team_id == team_id) \ .where(team_members.user_id == user_id) (is_creator,) = db.first( tm_query.fields(team_members.is_creator)) db.delete(tm_query) # If the user was the team creator, select the earliest member # as the new creator. if is_creator: query = db.query(team_members) \ .where(team_members.team_id == team_id) row = db.first( query.fields(team_members.user_id) .order_by(team_members.joined_at), for_update=True) if row is None: # Team has become empty, delete it. teams = db.tables.teams team_query = db.query(teams) \ .where(teams.id == team_id) db.delete(team_query) else: # Promote the selected user as the creator. new_creator_id = row[0] db.update( query.where(team_members.user_id == new_creator_id), {team_members.is_creator: True})
def get_task_instance_hint(db, attempt_id, query, now): # Load the attempt and check that it is still open. attempt = load_attempt(db, attempt_id, now) if attempt['is_closed']: raise ModelError('attempt is closed') # Load the participation and verify that the round is still open. participation = load_participation(db, attempt['participation_id']) round_ = load_round(db, participation['round_id'], now) if round_['status'] != 'open': raise ModelError('round not open') # Obtain task backend URL and Authorization header. round_task = load_round_task(db, attempt['round_task_id']) task = load_task(db, round_task['task_id']) backend_url = task['backend_url'] auth = task['backend_auth'] # Load the task instance. task_instance = load_task_instance(db, attempt_id, for_update=True) if task_instance is None: raise ModelError('no task instance') full_data = task_instance['full_data'] team_data = task_instance['team_data'] # Get the task backend to validate the hint request and apply the hint # from full_data onto team_data. print('grantHint query {}'.format(query)) result = task_grant_hint(backend_url, full_data, team_data, query, auth) # result: {success, task, full_task} print('grantHint result {}'.format(result)) team_data = result.get('task', team_data) full_data = result.get('full_task', full_data) # If successful, update the task instance. # 'full_data' is also updated in case the task needs to store extra private # information there. if result['success']: attrs = {} if team_data is not task_instance['team_data']: attrs['team_data'] = db.dump_json(team_data) if full_data is not task_instance['full_data']: attrs['full_data'] = db.dump_json(full_data) if len(attrs) != 0: attrs['updated_at'] = now task_instances = db.tables.task_instances db.update_row(task_instances, {'attempt_id': attempt_id}, attrs) return result['success']
def leave_team(db, user_id, team_id, now): """ Remove a user from their team. """ user = load_user(db, user_id, for_update=True) team_id = user['team_id'] # The user must be member of a team. if team_id is None: raise ModelError('no team') team = load_team(db, team_id) # Load the participation. participation_id = get_team_latest_participation_id(db, team_id) participation = load_participation(db, participation_id) round_id = participation['round_id'] round_ = load_round(db, round_id) # If the team already has an attempt (is_locked=True), verify # that the team remains valid if the user is removed. if team['is_locked']: if round_['allow_team_changes']: validate_team(db, team_id, round_id, without_member=user, now=now) else: raise ModelError('team is locked') # Clear the user's team_id. set_user_team_id(db, user_id, None) # Delete the team_members row. team_members = db.tables.team_members tm_query = db.query(team_members) \ .where(team_members.team_id == team_id) \ .where(team_members.user_id == user_id) (is_creator, ) = db.first(tm_query.fields(team_members.is_creator)) db.delete(tm_query) # If the user was the team creator, select the earliest member # as the new creator. if is_creator: query = db.query(team_members) \ .where(team_members.team_id == team_id) row = db.first(query.fields(team_members.user_id).order_by( team_members.joined_at), for_update=True) if row is None: # Team has become empty, delete it. teams = db.tables.teams team_query = db.query(teams) \ .where(teams.id == team_id) db.delete(team_query) else: # Promote the selected user as the creator. new_creator_id = row[0] db.update(query.where(team_members.user_id == new_creator_id), {team_members.is_creator: True})
def join_team(db, user_id, team_id, now): """ Add a user to a team. Registration for the team's round must be open. Return a boolean indicating if the team member was added. """ # Verify that the user does not already belong to a team. user = load_user(db, user_id, for_update=True) if user['team_id'] is not None: raise ModelError('already in a team') # Verify that the team exists. team = load_team(db, team_id) # Verify that the team is open. if not team['is_open']: # Team is closed (by its creator). raise ModelError('team is closed') # Load the participation. participation_id = get_team_latest_participation_id(db, team_id) participation = load_participation(db, participation_id) round_id = participation['round_id'] # Look up the badges that grant access to the team's round, to # figure out whether the user is qualified for that round. user_badges = user['badges'] badges = db.tables.badges if len(user_badges) == 0: is_qualified = False else: query = db.query(badges) \ .where(badges.round_id == round_id) \ .where(badges.symbol.in_(user_badges)) \ .where(badges.is_active) \ .fields(badges.id) is_qualified = db.scalar(query) is not None # If the team has already accessed a task instance (is_locked=True), # verify that the team remains valid if the user is added. if team['is_locked']: round_ = load_round(round_id) if round_['allow_team_changes']: user['is_qualified'] = is_qualified validate_team(db, team_id, round_id, with_member=user, now=now) else: raise ModelError('team is locked') # Create the team_members row. user_id = user['id'] add_team_member(db, team_id, user_id, now=now, is_qualified=is_qualified) # Update the user's team_id. set_user_team_id(db, user_id, team_id)
def create_attempt(db, participation_id, round_task_id, now): # Load the participation and round_task, check consistency. participation = load_participation(db, participation_id) round_task = load_round_task(db, round_task_id) if participation['round_id'] != round_task['round_id']: raise ModelError('round mismatch') # Check that round is open. round_ = load_round(db, round_task['round_id'], now=now) if round_['status'] != 'open': raise ModelError('round not open') attempt_id = get_current_attempt_id(db, participation_id, round_task_id) is_training = False if attempt_id is None: # Create the initial attempt (which may or may not be a training # attempt, depending on the task_round's options). # The team is not locked at this time and may still be changed # (depending on round options). This requires adding an access # code when a new member joins the team, and deleting an access # code when a member leaves the team. is_training = round_task['have_training_attempt'] else: attempt = load_attempt(db, attempt_id, now=now, for_update=True) if attempt['is_training']: # Current attempt is training. Team must pass training to # create a timed attempt. if attempt['is_unsolved']: raise ModelError('must pass training') else: # Timed attempts allow starting the next attempt immediately # only if completed. # if not attempt['is_completed']: most_recent_allowed = now - timedelta(minutes=5) if have_attempt_after(db, participation_id, round_task_id, most_recent_allowed): raise ModelError('attempt too soon') # Optionally limit the number of timed attempts. if round_task['max_timed_attempts'] is not None: n_attempts = count_timed_attempts(db, participation_id) if n_attempts >= round_task['max_timed_attempts']: raise ModelError('too many attempts') # Reset the is_current flag on the current attempt. set_attempt_current(db, attempt_id, is_current=False) # Find the greatest attempt ordinal for the (participation, round_task). attempts = db.tables.attempts query = db.query(attempts) \ .where(attempts.participation_id == participation_id) \ .where(attempts.round_task_id == round_task_id) \ .order_by(attempts.ordinal.desc()) \ .fields(attempts.ordinal) row = db.first(query) ordinal = 1 if row is None else (row[0] + 1) attempt_id = db.insert_row(attempts, { 'participation_id': participation_id, 'round_task_id': round_task_id, 'ordinal': ordinal, 'created_at': now, 'started_at': None, # set when task is accessed 'closes_at': None, 'is_current': True, 'is_training': is_training, 'is_unsolved': True }) generate_access_codes(db, participation['team_id'], attempt_id) return attempt_id
def view_requesting_user( db, user_id=None, participation_id=None, attempt_id=None, is_admin=False): now = datetime.utcnow() view = { 'now': now, 'is_admin': is_admin } if user_id is None: return view # # Add the user. # user = load_user(db, user_id) if user is None: return view view['user_id'] = user_id view['user'] = view_user(user) team_id = user['team_id'] # # Quick return path when the user has no team. # if team_id is None: # If the user has no team, we look for a round to which a # badge grants access. badges = user['badges'] round_ids = find_round_ids_with_badges(db, badges, now) if len(round_ids) > 0: # TODO: resolve this somehow, for example by returning # the round views to the user and letting them choose. # For now, pick the first one (which has the greatest id). round_id = round_ids[0] round_ = load_round(db, round_id, now) view['round'] = view_round(round_) return view # # Add the team and team members. # team = load_team(db, team_id) members = load_team_members(db, team['id'], users=True) team_view = view['team'] = view_team(team, members) # # Add the team's participations. # participations = load_team_participations(db, team_id) round_ids = set() for participation in participations: round_ids.add(participation['round_id']) rounds = load_rounds(db, round_ids, now) view['participations'] = [ view_team_participation( participation, rounds[participation['round_id']]) for participation in participations ] if len(participations) == 0: return view # Mark the lastest (or selected) participation as current. if participation_id is None: participation = participations[-1] else: participation = get_by_id(participations, participation_id) if participation is None: return view view['participation_id'] = participation['id'] for pview in view['participations']: if pview['id'] == participation['id']: pview['is_current'] = True # # Add the current participation's round. # round_id = participation['round_id'] round_ = rounds[round_id] view['round'] = view_round(round_) # # Add the tasks for the current round. # round_tasks = load_round_tasks(db, round_id) view['round']['task_ids'] = [str(rt['id']) for rt in round_tasks] round_task_views = view['round_tasks'] = { str(rt['id']): view_round_task(rt) for rt in round_tasks } region_id = team['region_id'] if round_['status'] == 'closed' and team['region_id'] is not None: region = load_region(db, region_id) national_count = count_teams_in_round(db, round_id) big_region_count = count_teams_in_round_big_region( db, round_id, region['big_region_code']) region_count = count_teams_in_round_region(db, round_id, region_id) view['ranking'] = { 'national': { 'rank': participation['rank_national'], 'count': national_count }, 'big_region': { 'name': region['big_region_name'], 'rank': participation['rank_big_regional'], 'count': big_region_count }, 'region': { 'name': region['name'], 'rank': participation['rank_regional'], 'count': region_count } } # XXX A team's validity should be checked against settings for a # competition rather than a round. causes = validate_members_for_round(members, round_) team_view['round_access'] = list(causes.keys()) team_view['is_invalid'] = len(causes) != 0 # Do not return attempts if the team is invalid. if team_view['is_invalid']: return view # Load the participation attempts. attempts = load_participation_attempts(db, participation['id'], now) view_task_attempts(attempts, round_task_views) print("attempts {} {}".format(attempt_id, attempts)) # Find the requested attempt. current_attempt = get_by_id(attempts, attempt_id) if current_attempt is None: return view view['attempt_id'] = attempt_id # Focus on the current attempt. current_round_task = round_task_views[str(current_attempt['round_task_id'])] current_attempt_view = None for attempt_view in current_round_task['attempts']: if attempt_id == attempt_view.get('id'): current_attempt_view = attempt_view view['attempt'] = current_attempt_view view['round_task'] = current_round_task # XXX duplicates attempts :( if False: # Access codes are disabled members_view = view['team']['members'] access_codes = load_unlocked_access_codes(db, attempt_id) add_members_access_codes(members_view, access_codes) if current_attempt['is_training']: needs_codes = not have_one_code(members_view) else: needs_codes = not have_code_majority(members_view) current_attempt_view['needs_codes'] = needs_codes # Add task instance data, if available. try: # XXX Previously load_task_instance_team_data which did not parse # full_data. # /!\ task contains sensitive data # XXX If the round is closed, load and pass full_data? task_instance = load_user_task_instance(db, attempt_id) except ModelError: return view # If the round has a time limit, return the countdown. if round_['duration'] is not None: started_at = participation['started_at'] if started_at is not None: duration = timedelta(minutes=round_['duration']) countdown = started_at + duration view['countdown'] = started_at + duration if countdown < now: return view view['team_data'] = task_instance['team_data'] # Add a list of the workspace revisions for this attempt. add_revisions(db, view, attempt_id) # Give the user the id of their latest revision for the # current attempt, to be loaded into the crypto tab on # first access. revision_id = load_user_latest_revision_id( db, user_id, attempt_id) view['my_latest_revision_id'] = revision_id return view
def assign_task_instance(db, attempt_id, now): """ Assign a task to the attempt. The team performing the attempt must be valid, otherwise an exception is raised. """ attempt = load_attempt(db, attempt_id, now=now) if attempt['started_at'] is not None: return ModelError('already have a task') participation_id = attempt['participation_id'] participation = load_participation(db, participation_id) # The team must be valid to obtain a task. team_id = participation['team_id'] round_id = participation['round_id'] validate_team(db, team_id, round_id, now=now) # Verify that the round is open for training (and implicitly for # timed attempts). round_ = load_round(db, round_id, now=now) if round_['status'] != 'open': raise ModelError('round not open') if now < round_['training_opens_at']: raise ModelError('training is not open') # Load the round_task round_task_id = attempt['round_task_id'] round_task = load_round_task(db, round_task_id) # TODO: check round_task['have_training_attempt'] if next ordinal is 1 # TODO: check round_task['max_timed_attempts'] if next ordinal is >1 task = load_task(db, round_task['task_id']) # backend_url backend_url = task['backend_url'] auth = task['backend_auth'] task_params = round_task['generate_params'] seed = str(attempt_id) # TODO add a participation-specific key to the seed team_data, full_data = task_generate(backend_url, task_params, seed, auth) try: # Lock the task_instances table to prevent concurrent inserts. db.execute('LOCK TABLES task_instances WRITE, attempts READ').close() attrs = { 'attempt_id': attempt_id, 'created_at': now, 'updated_at': now, 'full_data': db.dump_json(full_data), 'team_data': db.dump_json(team_data) } task_instances = db.tables.task_instances db.insert_row(task_instances, attrs) finally: db.execute('UNLOCK TABLES').close() # Update the attempt. attempt_attrs = {'started_at': now} attempt_duration = round_task['attempt_duration'] if attempt_duration is not None: # Set the closing time on the attempt. attempt_attrs['closes_at'] = now + timedelta(minutes=attempt_duration) attempts = db.tables.attempts db.update_row(attempts, attempt_id, attempt_attrs) # If starting the first timed attempt, set started_at on the participation. if attempt['is_training'] == 0 and attempt['ordinal'] == 1: update_participation(db, participation_id, {'started_at': now}) # Create an initial workspace for the attempt. create_attempt_workspace(db, attempt_id, now)
def view_requesting_user(db, user_id=None, participation_id=None, attempt_id=None, is_admin=False): now = datetime.utcnow() view = {'now': now, 'is_admin': is_admin} if user_id is None: return view # # Add the user. # user = load_user(db, user_id) if user is None: return view view['user_id'] = user_id view['user'] = view_user(user) team_id = user['team_id'] # # Quick return path when the user has no team. # if team_id is None: # If the user has no team, we look for a round to which a # badge grants access. badges = user['badges'] round_ids = find_round_ids_with_badges(db, badges, now) if len(round_ids) > 0: # TODO: resolve this somehow, for example by returning # the round views to the user and letting them choose. # For now, pick the first one (which has the greatest id). round_id = round_ids[0] round_ = load_round(db, round_id, now) view['round'] = view_round(round_) return view # # Add the team and team members. # team = load_team(db, team_id) members = load_team_members(db, team['id'], users=True) team_view = view['team'] = view_team(team, members) # # Add the team's participations. # participations = load_team_participations(db, team_id) round_ids = set() for participation in participations: round_ids.add(participation['round_id']) rounds = load_rounds(db, round_ids, now) view['participations'] = [ view_team_participation(participation, rounds[participation['round_id']]) for participation in participations ] if len(participations) == 0: return view # Mark the lastest (or selected) participation as current. if participation_id is None: participation = participations[-1] else: participation = get_by_id(participations, participation_id) if participation is None: return view view['participation_id'] = participation['id'] for pview in view['participations']: if pview['id'] == participation['id']: pview['is_current'] = True # # Add the current participation's round. # round_id = participation['round_id'] round_ = rounds[round_id] view['round'] = view_round(round_) # # Add the tasks for the current round. # round_tasks = load_round_tasks(db, round_id) view['round']['task_ids'] = [str(rt['id']) for rt in round_tasks] round_task_views = view['round_tasks'] = { str(rt['id']): view_round_task(rt) for rt in round_tasks } region_id = team['region_id'] if round_['status'] == 'closed' and team['region_id'] is not None: region = load_region(db, region_id) national_count = count_teams_in_round(db, round_id) big_region_count = count_teams_in_round_big_region( db, round_id, region['big_region_code']) region_count = count_teams_in_round_region(db, round_id, region_id) view['ranking'] = { 'national': { 'rank': participation['rank_national'], 'count': national_count }, 'big_region': { 'name': region['big_region_name'], 'rank': participation['rank_big_regional'], 'count': big_region_count }, 'region': { 'name': region['name'], 'rank': participation['rank_regional'], 'count': region_count } } # XXX A team's validity should be checked against settings for a # competition rather than a round. causes = validate_members_for_round(members, round_) team_view['round_access'] = list(causes.keys()) team_view['is_invalid'] = len(causes) != 0 # Do not return attempts if the team is invalid. if team_view['is_invalid']: return view # Load the participation attempts. attempts = load_participation_attempts(db, participation['id'], now) view_task_attempts(attempts, round_task_views) print("attempts {} {}".format(attempt_id, attempts)) # Find the requested attempt. current_attempt = get_by_id(attempts, attempt_id) if current_attempt is None: return view view['attempt_id'] = attempt_id # Focus on the current attempt. current_round_task = round_task_views[str( current_attempt['round_task_id'])] current_attempt_view = None for attempt_view in current_round_task['attempts']: if attempt_id == attempt_view.get('id'): current_attempt_view = attempt_view view['attempt'] = current_attempt_view view['round_task'] = current_round_task # XXX duplicates attempts :( if False: # Access codes are disabled members_view = view['team']['members'] access_codes = load_unlocked_access_codes(db, attempt_id) add_members_access_codes(members_view, access_codes) if current_attempt['is_training']: needs_codes = not have_one_code(members_view) else: needs_codes = not have_code_majority(members_view) current_attempt_view['needs_codes'] = needs_codes # Add task instance data, if available. try: # XXX Previously load_task_instance_team_data which did not parse # full_data. # /!\ task contains sensitive data # XXX If the round is closed, load and pass full_data? task_instance = load_user_task_instance(db, attempt_id) except ModelError: return view # If the round has a time limit, return the countdown. if round_['duration'] is not None: started_at = participation['started_at'] if started_at is not None: duration = timedelta(minutes=round_['duration']) countdown = started_at + duration view['countdown'] = started_at + duration if countdown < now: return view view['team_data'] = task_instance['team_data'] # Add a list of the workspace revisions for this attempt. add_revisions(db, view, attempt_id) # Give the user the id of their latest revision for the # current attempt, to be loaded into the crypto tab on # first access. revision_id = load_user_latest_revision_id(db, user_id, attempt_id) view['my_latest_revision_id'] = revision_id return view
def grade_answer(db, attempt_id, submitter_id, revision_id, data, now): attempt = load_attempt(db, attempt_id, now) is_training = attempt['is_training'] participation_id = attempt['participation_id'] participation = load_participation( db, participation_id, for_update=True) round_task = load_round_task(db, attempt['round_task_id']) task = load_task(db, round_task['task_id']) # backend_url # Fail if the attempt is closed. if attempt['is_closed']: raise ModelError('attempt is closed') # Get the greatest ordinal and nth most recent submitted_at. (prev_ordinal, nth_submitted_at) = \ get_attempt_latest_answer_infos(db, attempt_id, nth=2) # Fail if timed(not training) and there are more answers than # allowed. round_ = load_round(db, participation['round_id'], now) if round_['status'] != 'open': raise ModelError('round not open') max_answers = round_task['max_attempt_answers'] if (not is_training and max_answers is not None and prev_ordinal >= max_answers): raise ModelError('too many answers') # Fail if answer was submitted too recently. if nth_submitted_at is not None: if now < nth_submitted_at + timedelta(minutes=1): raise ModelError('too soon') ordinal = prev_ordinal + 1 # Perform grading. backend_url = task['backend_url'] auth = task['backend_auth'] task_instance = load_task_instance(db, attempt_id) full_data = task_instance['full_data'] team_data = task_instance['team_data'] grading = task_grade_answer(backend_url, full_data, team_data, data, auth) # grading: {feedback, score, is_solution, is_full_solution} # Update the attempt to indicate if solved, fully solved. update_attempt_with_grading(db, attempt_id, grading) # Store the answer and grading. answers = db.tables.answers answer = { 'attempt_id': attempt_id, 'submitter_id': submitter_id, 'ordinal': ordinal, 'created_at': now, 'answer': db.dump_json(data), 'grading': db.dump_json(grading), 'score': grading['score'], 'is_solution': grading['is_solution'], 'is_full_solution': grading['is_full_solution'], 'revision_id': revision_id } answer['id'] = db.insert_row(answers, answer) # Best score for the participation? new_score = Decimal(answer['score']) if not is_training and (participation['score'] is None or new_score > participation['score']): update_participation( db, participation_id, {'score': new_score}) return (answer, grading.get('feedback'))
def grade_answer(db, attempt_id, submitter_id, revision_id, data, now): attempt = load_attempt(db, attempt_id, now) is_training = attempt['is_training'] participation_id = attempt['participation_id'] participation = load_participation(db, participation_id, for_update=True) round_task = load_round_task(db, attempt['round_task_id']) task = load_task(db, round_task['task_id']) # backend_url # Fail if the attempt is closed. if attempt['is_closed']: raise ModelError('attempt is closed') # Get the greatest ordinal and nth most recent submitted_at. (prev_ordinal, nth_submitted_at) = \ get_attempt_latest_answer_infos(db, attempt_id, nth=2) # Fail if timed(not training) and there are more answers than # allowed. round_ = load_round(db, participation['round_id'], now) if round_['status'] != 'open': raise ModelError('round not open') max_answers = round_task['max_attempt_answers'] if (not is_training and max_answers is not None and prev_ordinal >= max_answers): raise ModelError('too many answers') # Fail if answer was submitted too recently. if nth_submitted_at is not None: if now < nth_submitted_at + timedelta(minutes=1): raise ModelError('too soon') ordinal = prev_ordinal + 1 # Perform grading. backend_url = task['backend_url'] auth = task['backend_auth'] task_instance = load_task_instance(db, attempt_id) full_data = task_instance['full_data'] team_data = task_instance['team_data'] grading = task_grade_answer(backend_url, full_data, team_data, data, auth) # grading: {feedback, score, is_solution, is_full_solution} # Update the attempt to indicate if solved, fully solved. update_attempt_with_grading(db, attempt_id, grading) # Store the answer and grading. answers = db.tables.answers answer = { 'attempt_id': attempt_id, 'submitter_id': submitter_id, 'ordinal': ordinal, 'created_at': now, 'answer': db.dump_json(data), 'grading': db.dump_json(grading), 'score': grading['score'], 'is_solution': grading['is_solution'], 'is_full_solution': grading['is_full_solution'], 'revision_id': revision_id } answer['id'] = db.insert_row(answers, answer) # Best score for the participation? new_score = Decimal(answer['score']) if not is_training and (participation['score'] is None or new_score > participation['score']): update_participation(db, participation_id, {'score': new_score}) return (answer, grading.get('feedback'))