Commit b4698e40 by Matjaz Gregoric

Add ability for staff to always view the results.

Course staff users can now view the results without voting.
The can also see the results for polls/surveys marked as 'private'.

If user has sufficient permissions, a 'View results' link is shown
on the bottom of the component in the LMS.
parent 9a8c6c65
...@@ -34,6 +34,11 @@ from xblock.fragment import Fragment ...@@ -34,6 +34,11 @@ from xblock.fragment import Fragment
from xblockutils.publish_event import PublishEventMixin from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
try:
from courseware.access import has_access
except ImportError:
has_access = None
class ResourceMixin(object): class ResourceMixin(object):
loader = ResourceLoader(__name__) loader = ResourceLoader(__name__)
...@@ -166,6 +171,16 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin): ...@@ -166,6 +171,16 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return True return True
return False return False
def can_view_private_results(self):
"""
Checks to see if the user has permissions to view private results.
This only works inside the LMS.
"""
if has_access and hasattr(self.runtime, 'user') and hasattr(self.runtime, 'course_id'):
return has_access(self.runtime.user, 'staff', self, self.runtime.course_id)
else:
return False
@staticmethod @staticmethod
def get_max_submissions(data, result, private_results): def get_max_submissions(data, result, private_results):
""" """
...@@ -305,6 +320,7 @@ class PollBlock(PollBase): ...@@ -305,6 +320,7 @@ class PollBlock(PollBase):
'can_vote': self.can_vote(), 'can_vote': self.can_vote(),
'max_submissions': self.max_submissions, 'max_submissions': self.max_submissions,
'submissions_count': self.submissions_count, 'submissions_count': self.submissions_count,
'can_view_private_results': self.can_view_private_results(),
}) })
if self.choice: if self.choice:
...@@ -346,7 +362,7 @@ class PollBlock(PollBase): ...@@ -346,7 +362,7 @@ class PollBlock(PollBase):
@XBlock.json_handler @XBlock.json_handler
def get_results(self, data, suffix=''): def get_results(self, data, suffix=''):
if self.private_results: if self.private_results and not self.can_view_private_results():
detail, total = {}, None detail, total = {}, None
else: else:
self.publish_event_from_dict(self.event_namespace + '.view_results', {}) self.publish_event_from_dict(self.event_namespace + '.view_results', {})
...@@ -512,6 +528,7 @@ class SurveyBlock(PollBase): ...@@ -512,6 +528,7 @@ class SurveyBlock(PollBase):
'can_vote': self.can_vote(), 'can_vote': self.can_vote(),
'submissions_count': self.submissions_count, 'submissions_count': self.submissions_count,
'max_submissions': self.max_submissions, 'max_submissions': self.max_submissions,
'can_view_private_results': self.can_view_private_results(),
}) })
return self.create_fragment( return self.create_fragment(
...@@ -554,7 +571,7 @@ class SurveyBlock(PollBase): ...@@ -554,7 +571,7 @@ class SurveyBlock(PollBase):
tally = [] tally = []
questions = OrderedDict(self.markdown_items(self.questions)) questions = OrderedDict(self.markdown_items(self.questions))
default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers]) default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers])
choices = self.choices choices = self.choices or {}
total = 0 total = 0
self.clean_tally() self.clean_tally()
source_tally = self.tally source_tally = self.tally
...@@ -585,7 +602,7 @@ class SurveyBlock(PollBase): ...@@ -585,7 +602,7 @@ class SurveyBlock(PollBase):
highest = 0 highest = 0
top_index = None top_index = None
for index, answer in enumerate(question['answers']): for index, answer in enumerate(question['answers']):
if answer['key'] == choices[question['key']]: if answer['key'] == choices.get(question['key']):
answer['choice'] = True answer['choice'] = True
# Find the most popular choice. # Find the most popular choice.
if answer['count'] > highest: if answer['count'] > highest:
...@@ -595,7 +612,8 @@ class SurveyBlock(PollBase): ...@@ -595,7 +612,8 @@ class SurveyBlock(PollBase):
answer['percent'] = round(answer['count'] / float(total) * 100) answer['percent'] = round(answer['count'] / float(total) * 100)
except ZeroDivisionError: except ZeroDivisionError:
answer['percent'] = 0 answer['percent'] = 0
question['answers'][top_index]['top'] = True if top_index is not None:
question['answers'][top_index]['top'] = True
return tally, total return tally, total
...@@ -661,7 +679,7 @@ class SurveyBlock(PollBase): ...@@ -661,7 +679,7 @@ class SurveyBlock(PollBase):
@XBlock.json_handler @XBlock.json_handler
def get_results(self, data, suffix=''): def get_results(self, data, suffix=''):
if self.private_results: if self.private_results and not self.can_view_private_results():
detail, total = {}, None detail, total = {}, None
else: else:
self.publish_event_from_dict(self.event_namespace + '.view_results', {}) self.publish_event_from_dict(self.event_namespace + '.view_results', {})
......
...@@ -211,3 +211,8 @@ th.survey-answer { ...@@ -211,3 +211,8 @@ th.survey-answer {
.poll-submissions-count { .poll-submissions-count {
font-weight: bold; font-weight: bold;
} }
.view-results-button-wrapper {
text-align: right;
cursor: pointer;
}
<script id="poll-results-template" type="text/html"> <script id="poll-results-template" type="text/html">
<h3 class="poll-header">{{display_name}}</h3> <h3 class="poll-header">{{display_name}}</h3>
<div class="poll-question-container">{{{question}}}</div> <div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results"> <ul class="poll-answers-results poll-results">
{{#each tally}} {{#each tally}}
<li class="poll-result"> <li class="poll-result">
<div class="poll-result-input-container"> <div class="poll-result-input-container">
......
<script id="survey-results-template" type="text/html"> <script id="survey-results-template" type="text/html">
<h3 class="poll-header">{{block_name}}</h3> <h3 class="poll-header">{{block_name}}</h3>
<table class="survey-table"> <table class="survey-table poll-results">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
......
...@@ -26,19 +26,23 @@ ...@@ -26,19 +26,23 @@
</ul> </ul>
<input class="input-main" type="button" name="poll-submit" value="{% if choice %}Resubmit{% else %}Submit{% endif %}" disabled /> <input class="input-main" type="button" name="poll-submit" value="{% if choice %}Resubmit{% else %}Submit{% endif %}" disabled />
</form> </form>
<div class="poll-voting-thanks{% if not choice or can_vote %} poll-hidden{% endif %}"><span>Thank you for your submission!</span></div> <div class="poll-voting-thanks{% if not choice or can_vote %} poll-hidden{% endif %}"><span>Thank you for your submission!</span></div>
<div class="poll-submissions-count poll-hidden"> <div class="poll-submissions-count poll-hidden">
You have used <span class="poll-current-count">{{ submissions_count }}</span> You have used <span class="poll-current-count">{{ submissions_count }}</span>
out of <span class="poll-max-submissions">{{ max_submissions }}</span> submissions. out of <span class="poll-max-submissions">{{ max_submissions }}</span> submissions.
</div>
{% if feedback %}
<div class="poll-feedback-container{% if not choice %} poll-hidden{% endif %}">
<hr />
<h3 class="poll-header">Feedback</h3>
<div class="poll-feedback">
{{feedback|safe}}
</div> </div>
{% if feedback %} </div>
<div class="poll-feedback-container{% if not choice %} poll-hidden{% endif %}"> {% endif %}
<hr />
<h3 class="poll-header">Feedback</h3> {% if can_view_private_results %}
<div class="poll-feedback"> <div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
{{feedback|safe}} {% endif %}
</div>
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
...@@ -47,5 +47,9 @@ ...@@ -47,5 +47,9 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if can_view_private_results %}
<div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
...@@ -10,10 +10,12 @@ function PollUtil (runtime, element, pollType) { ...@@ -10,10 +10,12 @@ function PollUtil (runtime, element, pollType) {
this.submit = $('input[type=button]', element); this.submit = $('input[type=button]', element);
this.answers = $('input[type=radio]', element); this.answers = $('input[type=radio]', element);
this.resultsTemplate = Handlebars.compile($("#" + pollType + "-results-template", element).html()); this.resultsTemplate = Handlebars.compile($("#" + pollType + "-results-template", element).html());
this.viewResultsButton = $('.view-results-button', element);
this.viewResultsButton.click(this.getResults);
// If the submit button doesn't exist, the user has already // If the submit button doesn't exist, the user has already
// selected a choice. Render results instead of initializing machinery. // selected a choice. Render results instead of initializing machinery.
if (! self.submit.length) { if (! self.submit.length) {
self.getResults({'success': true}); self.onSubmit({'success': true});
return false; return false;
} }
var max_submissions = parseInt($('.poll-max-submissions', element).text()); var max_submissions = parseInt($('.poll-max-submissions', element).text());
...@@ -42,7 +44,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -42,7 +44,7 @@ function PollUtil (runtime, element, pollType) {
type: "POST", type: "POST",
url: self.voteUrl, url: self.voteUrl,
data: JSON.stringify({"choice": choice}), data: JSON.stringify({"choice": choice}),
success: self.getResults success: self.onSubmit
}); });
}); });
// If the user has already reached their maximum submissions, all inputs should be disabled. // If the user has already reached their maximum submissions, all inputs should be disabled.
...@@ -73,7 +75,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -73,7 +75,7 @@ function PollUtil (runtime, element, pollType) {
type: "POST", type: "POST",
url: self.voteUrl, url: self.voteUrl,
data: JSON.stringify(self.surveyChoices()), data: JSON.stringify(self.surveyChoices()),
success: self.getResults success: self.onSubmit
}) })
}); });
// If the user has refreshed the page, they may still have an answer // If the user has refreshed the page, they may still have an answer
...@@ -86,7 +88,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -86,7 +88,7 @@ function PollUtil (runtime, element, pollType) {
var choices = {}; var choices = {};
self.answers.each(function(index, el) { self.answers.each(function(index, el) {
el = $(el); el = $(el);
choices[el.prop('name')] = $(self.checkedElement(el)).val(); choices[el.prop('name')] = $(self.checkedElement(el), element).val();
}); });
return choices; return choices;
}; };
...@@ -111,7 +113,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -111,7 +113,7 @@ function PollUtil (runtime, element, pollType) {
} }
}; };
this.getResults = function (data) { this.onSubmit = function (data) {
// Fetch the results from the server and render them. // Fetch the results from the server and render them.
if (!data['success']) { if (!data['success']) {
alert(data['errors'].join('\n')); alert(data['errors'].join('\n'));
...@@ -135,6 +137,11 @@ function PollUtil (runtime, element, pollType) { ...@@ -135,6 +137,11 @@ function PollUtil (runtime, element, pollType) {
return; return;
} }
// Used if results are not private, to show the user how other students voted. // Used if results are not private, to show the user how other students voted.
self.getResults();
};
this.getResults = function () {
// Used if results are not private, to show the user how other students voted.
$.ajax({ $.ajax({
// Semantically, this would be better as GET, but we can use helper // Semantically, this would be better as GET, but we can use helper
// functions with POST. // functions with POST.
......
...@@ -3,4 +3,4 @@ markdown ...@@ -3,4 +3,4 @@ markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils -e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
-e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev -e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev
ddt ddt
mock
...@@ -23,11 +23,29 @@ ...@@ -23,11 +23,29 @@
from ddt import ddt, unpack, data from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from poll.poll import PollBase
from base_test import PollBaseTest, DEFAULT_SURVEY_NAMES, DEFAULT_POLL_NAMES from base_test import PollBaseTest, DEFAULT_SURVEY_NAMES, DEFAULT_POLL_NAMES
scenarios = ('Survey Private', DEFAULT_SURVEY_NAMES), ('Poll Private', DEFAULT_POLL_NAMES) scenarios = ('Survey Private', DEFAULT_SURVEY_NAMES), ('Poll Private', DEFAULT_POLL_NAMES)
def stub_view_permission(can_view):
"""
Patches the 'can_view_private_results' function to return specified answer.
"""
def stub_view_permissions_decorator(test_fn):
def test_patched(self, page_name, names):
original = PollBase.can_view_private_results
try:
PollBase.can_view_private_results = lambda self: can_view
test_fn(self, page_name, names)
finally:
PollBase.can_view_private_results = original
return test_patched
return stub_view_permissions_decorator
@ddt @ddt
class TestPrivateResults(PollBaseTest): class TestPrivateResults(PollBaseTest):
""" """
...@@ -55,7 +73,7 @@ class TestPrivateResults(PollBaseTest): ...@@ -55,7 +73,7 @@ class TestPrivateResults(PollBaseTest):
self.do_submit(names) self.do_submit(names)
# No results should be showing. # No results should be showing.
self.assertNotIn(self.browser.find_element_by_css_selector('div.poll-block').get_attribute('innerHTML'), 'poll-top-choice') self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-results')
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-footnote') self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-footnote')
@unpack @unpack
...@@ -83,3 +101,27 @@ class TestPrivateResults(PollBaseTest): ...@@ -83,3 +101,27 @@ class TestPrivateResults(PollBaseTest):
self.assertFalse(self.browser.find_element_by_css_selector('.poll-feedback').is_displayed()) self.assertFalse(self.browser.find_element_by_css_selector('.poll-feedback').is_displayed())
self.do_submit(names) self.do_submit(names)
self.assertTrue(self.browser.find_element_by_css_selector('.poll-feedback').is_displayed()) self.assertTrue(self.browser.find_element_by_css_selector('.poll-feedback').is_displayed())
@unpack
@data(*scenarios)
@stub_view_permission(False)
def test_results_button_visibility_without_permission(self, page_name, names):
self.go_to_page(page_name)
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.view-results-button')
@unpack
@data(*scenarios)
@stub_view_permission(True)
def test_results_button_visibility_with_permission(self, page_name, names):
self.go_to_page(page_name)
self.browser.find_element_by_css_selector('.view-results-button')
@unpack
@data(*scenarios)
@stub_view_permission(True)
def test_results_button(self, page_name, names):
self.go_to_page(page_name)
button = self.browser.find_element_by_css_selector('a.view-results-button')
button.click()
self.wait_until_exists('.poll-results')
self.wait_until_exists('.poll-footnote')
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