Skip to content

Commit

Permalink
[SOL-1944] Submitting answers in assessment mode
Browse files Browse the repository at this point in the history
  • Loading branch information
E. Kolpakov committed Aug 2, 2016
1 parent 76afa67 commit 44e31bc
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 128 deletions.
271 changes: 172 additions & 99 deletions drag_and_drop_v2/drag_and_drop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,101 +294,71 @@ def studio_submit(self, submissions, suffix=''):
}

@XBlock.json_handler
def do_attempt(self, attempt, suffix=''):
item = self._get_item_definition(attempt['val'])
def drop_item(self, item_attempt, suffix=''):
self._validate_drop_item(item_attempt)

state = None
zone = None
feedback = item['feedback']['incorrect']
overall_feedback = None
is_correct = False

if self._is_attempt_correct(attempt): # Student placed item in a correct zone
is_correct = True
feedback = item['feedback']['correct']
state = {
'zone': attempt['zone'],
'correct': True,
'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'],
}

if state:
self.item_state[str(item['id'])] = state
zone = self._get_zone_by_uid(state['zone'])
if self.mode == self.ASSESSMENT_MODE:
return self._drop_item_assessment(item_attempt)
elif self.mode == self.STANDARD_MODE:
return self._drop_item_standard(item_attempt)
else:
zone = self._get_zone_by_uid(attempt['zone'])
if not zone:
raise JsonHandlerError(400, "Item zone data invalid.")
raise JsonHandlerError(500, _("Unknown DnDv2 mode {mode} - course is misconfigured").format(self.mode))

if self._is_finished():
overall_feedback = self.data['feedback']['finish']
@XBlock.json_handler
def do_attempt(self, data, suffix=''):
self._validate_attempt()

# don't publish the grade if the student has already completed the problem
if not self.completed:
if self._is_finished():
self.completed = True
try:
self.runtime.publish(self, 'grade', {
'value': self._get_grade(),
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass
self.num_attempts += 1
self._mark_complete_and_publish_grade()

self.runtime.publish(self, 'edx.drag_and_drop_v2.item.dropped', {
'item_id': item['id'],
'location': zone.get("title"),
'location_id': zone.get("uid"),
'is_correct': is_correct,
})
__, placed, correct = self._get_item_raw_stats()
placed_ids, correct_ids = set(placed), set(correct)
misplaced_ids = placed_ids - correct_ids

if self.mode == self.ASSESSMENT_MODE:
# In assessment mode we don't send any feedback on drop.
result = {}
feedback_msgs = [_('Correctly placed {correct_count} items, misplaced {misplaced_count} items.').format(
correct_count=len(correct_ids),
misplaced_count=len(misplaced_ids)
)]

if misplaced_ids:
feedback_msgs.append(_('Misplaced items were returned to item bank.'))
else:
result = {
'correct': is_correct,
'finished': self._is_finished(),
'overall_feedback': overall_feedback,
'feedback': feedback
}
feedback_msgs.append(self.data['feedback']['finish'])

return result
for item_id in misplaced_ids:
del self.item_state[item_id]

if not self.attemps_remain:
feedback_msgs.append(_('Final attempt was used, final score is {score}').format(score=self._get_grade()))

return {
'num_attempts': self.num_attempts,
'misplaced_items': list(misplaced_ids),
'feedback': '\n'.join(feedback_msgs),
'attempts_remain': self.attemps_remain
}

def _validate_attempt(self):
if self.mode != self.ASSESSMENT_MODE:
raise JsonHandlerError(400, _("do_attempt handler should only be called for assessment mode"))
if not self.attemps_remain:
raise JsonHandlerError(409, _("Max number of attempts reached"))

@XBlock.json_handler
def publish_event(self, data, suffix=''):
try:
event_type = data.pop('event_type')
except KeyError:
return {'result': 'error', 'message': 'Missing event_type in JSON data'}

self.runtime.publish(self, event_type, data)
return {'result': 'success'}

@XBlock.json_handler
def reset(self, data, suffix=''):
self.item_state = {}
return self._get_user_state()

def _is_attempt_correct(self, attempt):
"""
Check if the item was placed correctly.
"""
correct_zones = self._get_item_zones(attempt['val'])
return attempt['zone'] in correct_zones

def _expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
This method is unfortunately a bit hackish since XBlock does not provide a low-level API
for this.
"""
if hasattr(self.runtime, 'replace_urls'):
url = self.runtime.replace_urls('"{}"'.format(url))[1:-1]
elif hasattr(self.runtime, 'course_id'):
# edX Studio uses a different runtime for 'studio_view' than 'student_view',
# and the 'studio_view' runtime doesn't provide the replace_urls API.
try:
from static_replace import replace_static_urls # pylint: disable=import-error
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass
return url

@XBlock.json_handler
def expand_static_url(self, url, suffix=''):
""" AJAX-accessible handler for expanding URLs to static [image] files """
Expand All @@ -412,12 +382,120 @@ def default_background_image_url(self):
""" The URL to the default background image, shown when no custom background is used """
return self.runtime.local_resource_url(self, "public/img/triangle.png")

@property
def attemps_remain(self):
return self.max_attempts is None or self.num_attempts < self.max_attempts

@XBlock.handler
def get_user_state(self, request, suffix=''):
""" GET all user-specific data, and any applicable feedback """
data = self._get_user_state()
return webob.Response(body=json.dumps(data), content_type='application/json')

def _drop_item_standard(self, item_attempt):
item = self._get_item_definition(item_attempt['val'])

is_correct = self._is_attempt_correct(item_attempt) # Student placed item in a correct zone
if is_correct: # In standard mode state is only updated when attempt is correct
self.item_state[str(item['id'])] = self._make_state_from_attempt(item_attempt, is_correct)

self._mark_complete_and_publish_grade()
self._publish_item_dropped_event(item_attempt, is_correct)

item_feedback_key = 'correct' if is_correct else 'incorrect'
item_feedback = item['feedback'][item_feedback_key]
overall_feedback = self.data['feedback']['finish'] if self._is_finished() else None

return {
'correct': is_correct,
'finished': self._is_finished(),
'overall_feedback': overall_feedback,
'feedback': item_feedback
}

def _drop_item_assessment(self, item_attempt):
if not self.attemps_remain:
raise JsonHandlerError(409, _("Max number of attempts reached"))

item = self._get_item_definition(item_attempt['val'])

is_correct = self._is_attempt_correct(item_attempt)
# State is always updated in assessment mode, to make do_attempt work correctly
self.item_state[str(item['id'])] = self._make_state_from_attempt(item_attempt, is_correct)

self._publish_item_dropped_event(item_attempt, self._is_attempt_correct(item_attempt))

return {}

def _validate_drop_item(self, item):
zone = self._get_zone_by_uid(item['zone'])
if not zone:
raise JsonHandlerError(400, "Item zone data invalid.")

@staticmethod
def _make_state_from_attempt(attempt, correct):
return {
'zone': attempt['zone'],
'correct': correct,
'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'],
}

def _mark_complete_and_publish_grade(self):
# don't publish the grade if the student has already completed the problem
if not self.completed:
self.completed = self._is_finished()
self._publish_grade()

def _publish_grade(self):
try:
self.runtime.publish(self, 'grade', {
'value': self._get_grade(),
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass

def _publish_item_dropped_event(self, attempt, is_correct):
item = self._get_item_definition(attempt['val'])
# attempt should already be validated here - not doing the check for existing zone again
zone = self._get_zone_by_uid(attempt['zone'])

self.runtime.publish(self, 'edx.drag_and_drop_v2.item.dropped', {
'item_id': item['id'],
'location': zone.get("title"),
'location_id': zone.get("uid"),
'is_correct': is_correct,
})

def _is_attempt_correct(self, attempt):
"""
Check if the item was placed correctly.
"""
correct_zones = self._get_item_zones(attempt['val'])
return attempt['zone'] in correct_zones

def _expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
This method is unfortunately a bit hackish since XBlock does not provide a low-level API
for this.
"""
if hasattr(self.runtime, 'replace_urls'):
url = self.runtime.replace_urls('"{}"'.format(url))[1:-1]
elif hasattr(self.runtime, 'course_id'):
# edX Studio uses a different runtime for 'studio_view' than 'student_view',
# and the 'studio_view' runtime doesn't provide the replace_urls API.
try:
from static_replace import replace_static_urls # pylint: disable=import-error
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass
return url

def _get_user_state(self):
""" Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state()
Expand Down Expand Up @@ -512,17 +590,22 @@ def _get_item_stats(self):
Returns a tuple representing the number of correctly-placed items,
and the total number of items that must be placed on the board (non-decoy items).
"""
all_items = self.data['items']
item_state = self._get_item_state()
required_items, _, correct_items = self._get_item_raw_stats()

required_items = [str(item['id']) for item in all_items if self._get_item_zones(item['id']) != []]
placed_items = [item for item in required_items if item in item_state]
correct_items = [item for item in placed_items if item_state[item]['correct']]
return len(correct_items), len(required_items)

def _get_item_raw_stats(self):
"""
Returns a 3-tuple containing required, placed and correct items.
"""
all_items = [str(item['id']) for item in self.data['items']]
item_state = self._get_item_state()

required_count = len(required_items)
correct_count = len(correct_items)
required_items = [item_id for item_id in all_items if self._get_item_zones(int(item_id)) != []]
placed_items = [item_id for item_id in all_items if item_id in item_state]
correct_items = [item_id for item_id in placed_items if item_state[item_id]['correct']]

return correct_count, required_count
return required_items, placed_items, correct_items

def _get_grade(self):
"""
Expand All @@ -539,16 +622,6 @@ def _is_finished(self):
correct_count, required_count = self._get_item_stats()
return correct_count == required_count

@XBlock.json_handler
def publish_event(self, data, suffix=''):
try:
event_type = data.pop('event_type')
except KeyError:
return {'result': 'error', 'message': 'Missing event_type in JSON data'}

self.runtime.publish(self, event_type, data)
return {'result': 'success'}

def _get_unique_id(self):
usage_id = self.scope_ids.usage_id
try:
Expand Down
Loading

0 comments on commit 44e31bc

Please sign in to comment.