Skip to content

Commit

Permalink
Delete learner's state in staff debug
Browse files Browse the repository at this point in the history
WIP
- implement the callback clear_student_state
- add a new submission to indicate existing answers are deleted
- add (but commented out) code to partially support revising score
manually.  But full support will require adding logic to persist the
score within xblock
- fix issue with same explanation displayed multiple times in Step 2
- simplify the Simple algo for picking sample explanation.  Instead of
using arbitrary max loop count of 100 and hope to pick unique explanations,
remove item from the pool once they are chosen.
  • Loading branch information
kitsook committed Jun 27, 2018
1 parent a8fdada commit 51965cc
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 38 deletions.
70 changes: 35 additions & 35 deletions ubcpi/answer_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,48 +227,45 @@ def get_other_answers_simple(pool, seeded_answers, get_student_item_dict, num_re
ret = []
# clean up answers so that all keys are int
pool = {int(k): v for k, v in pool.items()}
total_in_pool = len(seeded_answers)
merged_pool = convert_seeded_answers(seeded_answers)
student_id = get_student_item_dict()['student_id']
# merge the dictionaries in the answer dictionary
for key in pool:
total_in_pool += len(pool[key])
# if student_id has value, we assume the student just submitted an answer. So removing it
# from total number in the pool
if student_id in pool[key].keys():
total_in_pool -= 1
if key in merged_pool:
merged_pool[key].update(pool[key].items())
else:
merged_pool[key] = pool[key]

# remember which option+student_id is selected, so that we don't have duplicates in the result
selected = []
merged_pool.setdefault(key, {})
merged_pool[key].update(pool[key])
# Pop student's own answer, if exists
merged_pool[key].pop(student_id, None)

# loop until we have enough answers to return
while len(ret) < min(num_responses, total_in_pool):
# loop until we have enough answers to return or when there is nothing more to return
while len(ret) < num_responses and merged_pool:
for option, students in merged_pool.items():
student = student_id
i = 0
while (student == student_id or i > 100) and (str(option) + student) not in selected:
# retry until we got a different one or after 100 retries
# we are suppose to get a different student answer or a seeded one in a few tries
# as we have at least one seeded answer for each option in the algo. And it is not
# suppose to overflow i order to break the loop
rationale = None
while students:
student = random.choice(students.keys())
i += 1
selected.append(str(option)+student)
if student.startswith('seeded'):
# seeded answer, get the rationale from local
rationale = students[student]
else:
student_item = get_student_item_dict(student)
submission = sas_api.get_answers_for_student(student_item)
rationale = submission.get_rationale(0)
ret.append({'option': option, 'rationale': rationale})
# remove the chosen answer from pool
content = students.pop(student, None)

if student.startswith('seeded'):
# seeded answer, get the rationale from local
rationale = content
else:
student_item = get_student_item_dict(student)
submission = sas_api.get_answers_for_student(student_item)
# Make sure the answer is still the one we want.
# It may have changed (e.g. instructor deleted the student state
# and the student re-submitted a diff answer)
if submission.has_revision(0) and submission.get_vote(0) == option:
rationale = submission.get_rationale(0)

if rationale:
ret.append({'option': option, 'rationale': rationale})
break

if not students:
del merged_pool[option]

# check if we have enough answers
if len(ret) >= min(num_responses, total_in_pool):
if len(ret) >= num_responses:
break

return {"answers": ret}
Expand Down Expand Up @@ -316,8 +313,11 @@ def get_other_answers_random(pool, seeded_answers, get_student_item_dict, num_re
else:
student_item = get_student_item_dict(student)
submission = sas_api.get_answers_for_student(student_item)
rationale = submission.get_rationale(0)
option = submission.get_vote(0)
if submission.has_revision(0):
rationale = submission.get_rationale(0)
option = submission.get_vote(0)
else:
continue
ret.append({'option': option, 'rationale': rationale})

return {"answers": ret}
Expand Down
17 changes: 17 additions & 0 deletions ubcpi/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"""

ANSWER_LIST_KEY = 'answers'
DELETE_INDICATOR = 'deleted'
REQUEST_USER_ID_KEY = 'requesting_user_id'

VOTE_KEY = 'vote'
RATIONALE_KEY = 'rationale'
Expand All @@ -39,6 +41,8 @@ def get_answers_for_student(student_item):

latest_submission = submissions[0]
latest_answer_item = latest_submission.get('answer', {})
if latest_answer_item.get(DELETE_INDICATOR, False):
return Answers()
return Answers(latest_answer_item.get(ANSWER_LIST_KEY, []))


Expand All @@ -59,6 +63,19 @@ def add_answer_for_student(student_item, vote, rationale):
ANSWER_LIST_KEY: answers.get_answers_as_list()
})

def delete_answer_for_student(student_item, requesting_user_id):
"""
Create a new submission to indicate student's answer is deleted
Args:
student_item (dict): The location of the problem this submission is
associated with, as defined by a course, student, and item.
requesting_user_id: The user that is requesting to delete student answer
"""
sub_api.create_submission(student_item, {
DELETE_INDICATOR: True,
REQUEST_USER_ID_KEY: requesting_user_id,
})

class Answers:
"""
Expand Down
57 changes: 54 additions & 3 deletions ubcpi/ubcpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from webob import Response
from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
# For supporting manual revision of scores. Commented out for now.
# from xblock.scorable import ScorableXBlockMixin, Score
from xblock.fields import Scope, String, List, Dict, Integer, DateTime, Float
from xblock.fragment import Fragment
from xblockutils.publish_event import PublishEventMixin
Expand Down Expand Up @@ -256,8 +258,6 @@ class PeerInstructionXBlock(XBlock, MissingDataFetcherMixin, PublishEventMixin):
help=_("The algorithm for selecting which answers to be presented to students"),
)

# Declare that we are not part of the grading System. Disabled for now as for the concern about the loading
# speed of the progress page.
has_score = True

start = DateTime(
Expand Down Expand Up @@ -293,6 +293,57 @@ def max_score(self):
"""
return 1

# def calculate_score(self):
# answers = self.get_answers_for_student()
# if answers.has_revision(0) and answers.has_revision(1):
# return Score(1, 1)
# return Score(0, 1)
#
# def set_score(self, score):
# # TODO persisting score
# pass
#
# def get_score(self):
# # TODO Since we are not persisting score, always return 1.
# # That means the Overriding Score function will always set the score to 1
# # Instructors can reset the score to 0 by deleting learner's state
# return Score(1, 1)

def clear_student_state(self, user_id, course_id, item_id, requesting_user_id):
"""
Being notified that student state is going to be deleted. Mark student's
submissions as deleted
"""
student_item = dict(
student_id=user_id,
item_id=item_id,
course_id=course_id,
item_type='ubcpi'
)

# TODO currently not possible to revise the stats as they are defined with scope Scope.user_state_summary.
# The stats are not available when clear_student_state is called
# answers = sas_api.get_answers_for_student(student_item)
# stats = self.get_current_stats()
# if answers.has_revision(0):
# num_resp = stats['original'].setdefault(answers.get_vote(0), 0)
# if num_resp > 0:
# stats['original'][answers.get_vote(0)] = num_resp - 1
# if answers.has_revision(1):
# num_resp = stats['revised'].setdefault(answers.get_vote(1), 0)
# if num_resp > 0:
# stats['revised'][answers.get_vote(1)] = num_resp - 1

# mark existing submission as deleted
sas_api.delete_answer_for_student(student_item, requesting_user_id)

# def has_submitted_answer(self):
# answers = self.get_answers_for_student()
# return answers.has_revision(0) and answers.has_revision(1)
#
# def publish_grade(self):
# self._publish_grade(self.get_score())

def studio_view(self, context=None):
"""
view function for studio edit
Expand Down Expand Up @@ -619,7 +670,7 @@ def get_persisted_data(self, other_answers):
if answers.has_revision(0) and not answers.has_revision(1):
# If no persisted peer answers, generate new ones.
# Could happen if a student completed Step 1 before ubcpi upgraded to persist peer answers.
if not self.other_answers_shown:
if not other_answers:
ret['other_answers'] = get_other_answers(
self.sys_selected_answers, self.seeds, self.get_student_item_dict, self.algo, self.options)
else:
Expand Down

0 comments on commit 51965cc

Please sign in to comment.