Skip to content

Commit

Permalink
BB-726 Implement generate_report_data for PollBlock and SurveyBlock (#50
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
john2x committed Dec 26, 2018
1 parent 8a81d05 commit 291dce0
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 29 deletions.
77 changes: 77 additions & 0 deletions poll/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
48 changes: 26 additions & 22 deletions poll/translations/en/LC_MESSAGES/text.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -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 ""

Expand Down Expand Up @@ -118,15 +118,15 @@ 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 ""

#: poll/poll.py:462
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 ""

Expand All @@ -140,75 +140,79 @@ 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 ""

#: poll/poll.py:719
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."
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 ""
Expand Down
16 changes: 10 additions & 6 deletions poll/translations/eo/LC_MESSAGES/text.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -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éý Ⱡ'σяєм ιρѕυ#"
Expand Down Expand Up @@ -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 Ⱡ'σяєм ιρѕυ#"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
110 changes: 110 additions & 0 deletions tests/unit/test_xblock_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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)

0 comments on commit 291dce0

Please sign in to comment.