Commit fc465870 by Bogdan Licar Committed by GitHub

Merge pull request #163 from open-craft/bogdan-mrq-instructor tool

Added support for MRQ to the Instructor Tool data display and CSV export and updated test. Added user ID and user email to the CSV export
parent 2084f736
...@@ -135,6 +135,7 @@ class InstructorToolBlock(XBlock): ...@@ -135,6 +135,7 @@ class InstructorToolBlock(XBlock):
return Fragment(u'<p>This interface can only be used by course staff.</p>') return Fragment(u'<p>This interface can only be used by course staff.</p>')
block_choices = { block_choices = {
_('Multiple Choice Question'): 'MCQBlock', _('Multiple Choice Question'): 'MCQBlock',
_('Multiple Response Question'): 'MRQBlock',
_('Rating Question'): 'RatingBlock', _('Rating Question'): 'RatingBlock',
_('Long Answer'): 'AnswerBlock', _('Long Answer'): 'AnswerBlock',
} }
......
...@@ -28,6 +28,7 @@ from xblockutils.resources import ResourceLoader ...@@ -28,6 +28,7 @@ from xblockutils.resources import ResourceLoader
from problem_builder.mixins import StudentViewUserStateMixin from problem_builder.mixins import StudentViewUserStateMixin
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .sub_api import sub_api, SubmittingXBlockMixin
# Globals ########################################################### # Globals ###########################################################
...@@ -42,7 +43,7 @@ def _(text): ...@@ -42,7 +43,7 @@ def _(text):
# Classes ########################################################### # Classes ###########################################################
class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock): class MRQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAbstractBlock):
""" """
An XBlock used to ask multiple-response questions An XBlock used to ask multiple-response questions
""" """
...@@ -140,6 +141,7 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock): ...@@ -140,6 +141,7 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
choice_result = { choice_result = {
'value': choice.value, 'value': choice.value,
'selected': choice_selected, 'selected': choice_selected,
'content': choice.content
} }
# Only include tips/results in returned response if we want to display them # Only include tips/results in returned response if we want to display them
if not self.hide_results: if not self.hide_results:
...@@ -153,6 +155,11 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock): ...@@ -153,6 +155,11 @@ class MRQBlock(StudentViewUserStateMixin, QuestionnaireAbstractBlock):
status = 'incorrect' if score <= 0 else 'correct' if score >= len(results) else 'partial' status = 'incorrect' if score <= 0 else 'correct' if score >= len(results) else 'partial'
if sub_api:
# Send the answer as a concatenated list to the submissions API
answer = [choice['content'] for choice in results if choice['selected']]
sub_api.create_submission(self.student_item_key, ', '.join(answer))
return { return {
'submissions': submissions, 'submissions': submissions,
'status': status, 'status': status,
......
...@@ -251,7 +251,7 @@ function InstructorToolBlock(runtime, element) { ...@@ -251,7 +251,7 @@ function InstructorToolBlock(runtime, element) {
} }
// Block types with answers we can export // Block types with answers we can export
var questionBlockTypes = ['pb-mcq', 'pb-rating', 'pb-answer']; var questionBlockTypes = ['pb-mcq', 'pb-mrq', 'pb-rating', 'pb-answer'];
// Fetch this course's blocks from the REST API, and add them to the // Fetch this course's blocks from the REST API, and add them to the
// list of blocks in the Section/Question drop-down list. // list of blocks in the Section/Question drop-down list.
......
...@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore ...@@ -13,6 +13,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from .mcq import MCQBlock, RatingBlock from .mcq import MCQBlock, RatingBlock
from .mrq import MRQBlock
from problem_builder.answer import AnswerBlock from problem_builder.answer import AnswerBlock
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .sub_api import sub_api from .sub_api import sub_api
...@@ -23,7 +24,7 @@ logger = get_task_logger(__name__) ...@@ -23,7 +24,7 @@ logger = get_task_logger(__name__)
@task() @task()
def export_data(course_id, source_block_id_str, block_types, user_ids, match_string): def export_data(course_id, source_block_id_str, block_types, user_ids, match_string):
""" """
Exports student answers to all MCQ questions to a CSV file. Exports student answers to all supported questions to a CSV file.
""" """
start_timestamp = time.time() start_timestamp = time.time()
...@@ -36,7 +37,7 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str ...@@ -36,7 +37,7 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str
src_block = modulestore().get_item(usage_key) src_block = modulestore().get_item(usage_key)
course_key_str = unicode(course_key) course_key_str = unicode(course_key)
type_map = {cls.__name__: cls for cls in [MCQBlock, RatingBlock, AnswerBlock]} type_map = {cls.__name__: cls for cls in [MCQBlock, MRQBlock, RatingBlock, AnswerBlock]}
if not block_types: if not block_types:
block_types = tuple(type_map.values()) block_types = tuple(type_map.values())
...@@ -62,7 +63,9 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str ...@@ -62,7 +63,9 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str
# Define the header row of our CSV: # Define the header row of our CSV:
rows = [] rows = []
rows.append(["Section", "Subsection", "Unit", "Type", "Question", "Answer", "Username"]) rows.append(
["Section", "Subsection", "Unit", "Type", "Question", "Answer", "Username", "User ID", "User E-mail"]
)
# Collect results for each block in blocks_to_include # Collect results for each block in blocks_to_include
for block in blocks_to_include: for block in blocks_to_include:
...@@ -110,17 +113,27 @@ def _extract_data(course_key_str, block, user_id, match_string): ...@@ -110,17 +113,27 @@ def _extract_data(course_key_str, block, user_id, match_string):
# - Get all of the most recent student submissions for this block: # - Get all of the most recent student submissions for this block:
submissions = _get_submissions(course_key_str, block, user_id) submissions = _get_submissions(course_key_str, block, user_id)
# - For each submission, look up student's username and answer: # - For each submission, look up student's username, email and answer:
answer_cache = {} answer_cache = {}
for submission in submissions: for submission in submissions:
username = _get_username(submission, user_id) username, _user_id, user_email = _get_user_info(submission, user_id)
answer = _get_answer(block, submission, answer_cache) answer = _get_answer(block, submission, answer_cache)
# Short-circuit if answer does not match search criteria # Short-circuit if answer does not match search criteria
if not match_string.lower() in answer.lower(): if not match_string.lower() in answer.lower():
continue continue
rows.append([section_name, subsection_name, unit_name, block_type, block_question, answer, username]) rows.append([
section_name,
subsection_name,
unit_name,
block_type,
block_question,
answer,
username,
_user_id,
user_email
])
return rows return rows
...@@ -177,19 +190,19 @@ def _get_submissions(course_key_str, block, user_id): ...@@ -177,19 +190,19 @@ def _get_submissions(course_key_str, block, user_id):
return sub_api.get_submissions(student_dict, limit=1) return sub_api.get_submissions(student_dict, limit=1)
def _get_username(submission, user_id): def _get_user_info(submission, user_id):
""" """
Return username of student who provided `submission`. Return a (username, user id, user email) tuple for the student who provided `submission`.
If the anonymous id of the submission can't be resolved into a username, the anonymous If the anonymous ID of the submission can't be resolved into a user,
id is returned. (student ID, 'N/A', 'N/A') is returned
""" """
# If the student ID key doesn't exist, we're dealing with a single student and know the ID already. # If the student ID key doesn't exist, we're dealing with a single student and know the ID already.
student_id = submission.get('student_id', user_id) student_id = submission.get('student_id', user_id)
user = user_by_anonymous_id(student_id) user = user_by_anonymous_id(student_id)
if user is None: if user is None:
return student_id return (student_id, 'N/A', 'N/A')
return user.username return (user.username, user.id, user.email)
def _get_answer(block, submission, answer_cache): def _get_answer(block, submission, answer_cache):
......
...@@ -48,6 +48,7 @@ class TestInstructorToolBlock(unittest.TestCase): ...@@ -48,6 +48,7 @@ class TestInstructorToolBlock(unittest.TestCase):
""" """
block_choices = { block_choices = {
'Multiple Choice Question': 'MCQBlock', 'Multiple Choice Question': 'MCQBlock',
'Multiple Response Question': 'MRQBlock',
'Rating Question': 'RatingBlock', 'Rating Question': 'RatingBlock',
'Long Answer': 'AnswerBlock', 'Long Answer': 'AnswerBlock',
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment