From 291dce01adf384b98481f7975aee110224852a09 Mon Sep 17 00:00:00 2001 From: John Louis Del Rosario Date: Wed, 26 Dec 2018 13:27:55 +0800 Subject: [PATCH] BB-726 Implement generate_report_data for PollBlock and SurveyBlock (#50) * Implement generate_report_data methods for Poll and Survey blocks See BB-726 * Stop iterator when limit_responses is reached * Include submissions count in Poll report data * Add unit tests for generate_report_data * Add translations * Address PR comments - Remove Question ID and Answer ID from reports - Improve limit_responses check - Increase survey test to check for 4 items instead of 3 * Version bump to 1.6.4 * Update translations * Bump to 1.7.0 --- poll/poll.py | 77 ++++++++++++++++ poll/translations/en/LC_MESSAGES/text.po | 48 +++++----- poll/translations/eo/LC_MESSAGES/text.po | 16 ++-- setup.py | 2 +- tests/unit/test_xblock_poll.py | 110 +++++++++++++++++++++++ 5 files changed, 224 insertions(+), 29 deletions(-) diff --git a/poll/poll.py b/poll/poll.py index a73edc7..064fbd4 100644 --- a/poll/poll.py +++ b/poll/poll.py @@ -784,6 +784,42 @@ def prepare_data(self): ] return [header_row] + data.values() + def generate_report_data(self, user_state_iterator, limit_responses=None): + """ + Return a list of student responses to this block in a readable way. + + Arguments: + user_state_iterator: iterator over UserStateClient objects. + E.g. the result of user_state_client.iter_all_for_block(block_key) + + limit_responses (int|None): maximum number of responses to include. + Set to None (default) to include all. + + Returns: + each call returns a tuple like: + ("username", { + "Question": "What's your favorite color?" + "Answer": "Red", + "Submissions count": 1 + }) + """ + count = 0 + answers_dict = dict(self.answers) + for user_state in user_state_iterator: + + if limit_responses is not None and count >= limit_responses: + # End the iterator here + return + + choice = user_state.state['choice'] # {u'submissions_count': 1, u'choice': u'R'} + report = { + self.ugettext('Question'): self.question, + self.ugettext('Answer'): answers_dict[choice]['label'], + self.ugettext('Submissions count'): user_state.state['submissions_count'] + } + count += 1 + yield (user_state.username, report) + @XBlock.wants('settings') @XBlock.needs('i18n') @@ -1236,3 +1272,44 @@ def prepare_data(self): row.append(answers_dict[choice]) data[sm.student.id] = row return [header_row + questions] + data.values() + + def generate_report_data(self, user_state_iterator, limit_responses=None): + """ + Return a list of student responses to this block in a readable way. + + Arguments: + user_state_iterator: iterator over UserStateClient objects. + E.g. the result of user_state_client.iter_all_for_block(block_key) + + limit_responses (int|None): maximum number of responses to include. + Set to None (default) to include all. + + Returns: + each call returns a tuple like: + ("username", { + "Question": "Are you having fun?" + "Answer": "Yes", + "Submissions count": 1 + }) + """ + answers_dict = dict(self.answers) + questions_dict = dict(self.questions) + count = 0 + for user_state in user_state_iterator: + # user_state.state={'submissions_count': 1, 'choices': {u'enjoy': u'Y', u'recommend': u'N', u'learn': u'M'}} + choices = user_state.state['choices'] + for question_id, answer_id in choices.items(): + + if limit_responses is not None and count >= limit_responses: + # End the iterator here + return + + question = questions_dict[question_id]['label'] + answer = answers_dict[answer_id] + report = { + self.ugettext('Question'): question, + self.ugettext('Answer'): answer, + self.ugettext('Submissions count'): user_state.state['submissions_count'] + } + count += 1 + yield (user_state.username, report) diff --git a/poll/translations/en/LC_MESSAGES/text.po b/poll/translations/en/LC_MESSAGES/text.po index 0eaf7b4..cef1433 100644 --- a/poll/translations/en/LC_MESSAGES/text.po +++ b/poll/translations/en/LC_MESSAGES/text.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-11-14 12:06+0000\n" +"POT-Creation-Date: 2018-12-25 21:42+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -90,7 +90,7 @@ msgstr "" msgid "Private results may not be False when Maximum Submissions is not 1." msgstr "" -#: poll/poll.py:446 poll/poll.py:796 +#: poll/poll.py:446 poll/poll.py:832 msgid "Poll" msgstr "" @@ -118,7 +118,7 @@ msgstr "" msgid "The answer options on this poll." msgstr "" -#: poll/poll.py:461 poll/poll.py:822 +#: poll/poll.py:461 poll/poll.py:858 msgid "Total tally of answers from students." msgstr "" @@ -126,7 +126,7 @@ msgstr "" msgid "The student's answer" msgstr "" -#: poll/poll.py:666 poll/poll.py:1101 +#: poll/poll.py:666 poll/poll.py:1137 msgid "You have already voted in this poll." msgstr "" @@ -140,7 +140,7 @@ msgstr "" msgid "No key \"{choice}\" in answers table." msgstr "" -#: poll/poll.py:689 poll/poll.py:1109 +#: poll/poll.py:689 poll/poll.py:1145 msgid "You have already voted as many times as you are allowed." msgstr "" @@ -148,51 +148,59 @@ msgstr "" msgid "You must specify a question." msgstr "" -#: poll/poll.py:722 poll/poll.py:1163 +#: poll/poll.py:722 poll/poll.py:817 poll/poll.py:1199 poll/poll.py:1311 msgid "Answer" msgstr "" -#: poll/poll.py:793 +#: poll/poll.py:816 poll/poll.py:1200 poll/poll.py:1310 +msgid "Question" +msgstr "" + +#: poll/poll.py:818 poll/poll.py:1312 +msgid "Submissions count" +msgstr "" + +#: poll/poll.py:829 msgid "Survey" msgstr "" -#: poll/poll.py:799 +#: poll/poll.py:835 msgid "Yes" msgstr "" -#: poll/poll.py:800 +#: poll/poll.py:836 msgid "No" msgstr "" -#: poll/poll.py:801 +#: poll/poll.py:837 msgid "Maybe" msgstr "" -#: poll/poll.py:803 +#: poll/poll.py:839 msgid "Answer choices for this Survey" msgstr "" -#: poll/poll.py:807 +#: poll/poll.py:843 msgid "Are you enjoying the course?" msgstr "" -#: poll/poll.py:809 +#: poll/poll.py:845 msgid "Would you recommend this course to your friends?" msgstr "" -#: poll/poll.py:813 +#: poll/poll.py:849 msgid "Do you think you will learn a lot?" msgstr "" -#: poll/poll.py:815 +#: poll/poll.py:851 msgid "Questions for this Survey" msgstr "" -#: poll/poll.py:824 +#: poll/poll.py:860 msgid "The user's answers" msgstr "" -#: poll/poll.py:1117 +#: poll/poll.py:1153 msgid "" "Not all questions were included, or unknown questions were included. Try " "refreshing and trying again." @@ -200,15 +208,11 @@ msgstr "" #. Translators: {answer_key} uniquely identifies a specific answer belonging to a poll or survey. #. {question_key} uniquely identifies a specific question belonging to a poll or survey. -#: poll/poll.py:1130 +#: poll/poll.py:1166 #, python-brace-format msgid "Found unknown answer '{answer_key}' for question key '{question_key}'" msgstr "" -#: poll/poll.py:1164 -msgid "Question" -msgstr "" - #: poll/public/html/poll.html:35 poll/public/html/survey.html:53 msgid "Submit" msgstr "" diff --git a/poll/translations/eo/LC_MESSAGES/text.po b/poll/translations/eo/LC_MESSAGES/text.po index 91dfab8..4bb2c12 100644 --- a/poll/translations/eo/LC_MESSAGES/text.po +++ b/poll/translations/eo/LC_MESSAGES/text.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-11-14 12:06+0000\n" +"POT-Creation-Date: 2018-12-25 21:42+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -197,10 +197,18 @@ msgstr "" msgid "You must specify a question." msgstr "Ýöü müst spéçïfý ä qüéstïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" -#: poll/poll.py poll/poll.py +#: poll/poll.py poll/poll.py poll/poll.py poll/poll.py msgid "Answer" msgstr "Ànswér Ⱡ'σяєм ιρѕυ#" +#: poll/poll.py poll/poll.py poll/poll.py +msgid "Question" +msgstr "Qüéstïön Ⱡ'σяєм ιρѕυм ∂#" + +#: poll/poll.py poll/poll.py +msgid "Submissions count" +msgstr "Süßmïssïöns çöünt Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" + #: poll/poll.py msgid "Survey" msgstr "Sürvéý Ⱡ'σяєм ιρѕυ#" @@ -263,10 +271,6 @@ msgstr "" "Föünd ünknöwn änswér '{answer_key}' för qüéstïön kéý '{question_key}' Ⱡ'σяєм" " ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" -#: poll/poll.py -msgid "Question" -msgstr "Qüéstïön Ⱡ'σяєм ιρѕυм ∂#" - #: poll/public/html/poll.html poll/public/html/survey.html msgid "Submit" msgstr "Süßmït Ⱡ'σяєм ιρѕυ#" diff --git a/setup.py b/setup.py index cf61958..ec64179 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def package_data(pkg, roots): setup( name='xblock-poll', - version='1.6.3', + version='1.7.0', description='An XBlock for polling users.', packages=[ 'poll', diff --git a/tests/unit/test_xblock_poll.py b/tests/unit/test_xblock_poll.py index c608198..2a5dece 100644 --- a/tests/unit/test_xblock_poll.py +++ b/tests/unit/test_xblock_poll.py @@ -2,6 +2,7 @@ import json from xblock.field_data import DictFieldData +from mock import Mock from poll.poll import PollBlock, SurveyBlock from ..utils import MockRuntime, make_request @@ -69,6 +70,45 @@ def test_student_view_user_state_handler(self): } self.assertEqual(response, expected_response) + @classmethod + def mock_user_states(cls): + return ( + Mock(username='edx', state={'submissions_count': 1, 'choice': 'R'}), + Mock(username='verified', state={'submissions_count': 1, 'choice': 'G'}), + Mock(username='staff', state={'submissions_count': 1, 'choice': 'B'}), + Mock(username='honor', state={'submissions_count': 1, 'choice': 'O'}), + ) + + def test_generate_report_data_dont_limit_responses(self): + """ + Test generate_report_data iterator with no limit. + """ + user_states = self.mock_user_states() + report_data = self.poll_block.generate_report_data(user_states) + report_data = list(report_data) + self.assertEqual(len(report_data), 4) + self.assertEqual(report_data[0], + ('edx', {'Question': self.poll_block.question, + 'Answer': 'Red', + 'Submissions count': 1})) + + def test_generate_report_data_limit_responses(self): + """ + Test generate_report_data iterator with limit. + """ + user_states = self.mock_user_states() + report_data = self.poll_block.generate_report_data(user_states, limit_responses=2) + report_data = list(report_data) + self.assertEqual(len(report_data), 2) + self.assertEqual(report_data[0], + ('edx', {'Question': self.poll_block.question, + 'Answer': 'Red', + 'Submissions count': 1})) + + report_data = self.poll_block.generate_report_data(user_states, limit_responses=0) + report_data = list(report_data) + self.assertEqual(len(report_data), 0) + class TestSurveyBlock(unittest.TestCase): """ @@ -140,3 +180,73 @@ def test_student_view_user_state_handler(self): }, } self.assertEqual(response, expected_response) + + @classmethod + def mock_user_states(cls): + return ( + Mock( + username='edx', + state={ + 'submissions_count': 1, + 'choices': {'enjoy': 'Y', 'recommend': 'N', 'learn': 'M'} + } + ), + Mock( + username='verified', + state={ + 'submissions_count': 1, + 'choices': {'enjoy': 'M', 'recommend': 'N', 'learn': 'Y'} + } + ), + Mock( + username='staff', + state={ + 'submissions_count': 1, + 'choices': {'enjoy': 'N', 'recommend': 'N', 'learn': 'N'} + } + ), + Mock( + username='honor', + state={ + 'submissions_count': 1, + 'choices': {'enjoy': 'Y', 'recommend': 'N', 'learn': 'M'} + } + ), + ) + + def test_generate_report_data_dont_limit_responses(self): + """ + Test generate_report_data iterator with no limit. + """ + user_states = self.mock_user_states() + report_data = self.survey_block.generate_report_data(user_states) + report_data = list(report_data) + self.assertEqual(len(report_data), 12) + # each choice of a user gets its own row + # so the first three rows should be edx's choices + self.assertEqual(['edx', 'edx', 'edx', 'verified'], + [username for username, _ in report_data[:4]]) + self.assertEqual( + set(['Yes', 'No', 'Maybe']), + set([data['Answer'] for _, data in report_data[:4]]) + ) + + def test_generate_report_data_limit_responses(self): + """ + Test generate_report_data iterator with limit. + """ + user_states = self.mock_user_states() + report_data = self.survey_block.generate_report_data(user_states, limit_responses=2) + report_data = list(report_data) + self.assertEqual(len(report_data), 2) + # each choice of a user gets its own row + # so the first two rows should be edx's choices + self.assertEqual(['edx', 'edx'], + [username for username, _ in report_data]) + self.assertTrue( + set([data['Answer'] for _, data in report_data[:3]]) <= set(['Yes', 'No', 'Maybe']) + ) + + report_data = self.survey_block.generate_report_data(user_states, limit_responses=0) + report_data = list(report_data) + self.assertEqual(len(report_data), 0)