Commit ed6a2086 by Matjaz Gregoric

Merge pull request #10 from open-craft/show-private-results

Add ability for course staff to always view the results
parents 0b99e802 b37deed5
language: python
python:
- "2.7"
before_install:
- "export DISPLAY=:99"
- "sh -e /etc/init.d/xvfb start"
install:
- "sh install_test_deps.sh"
- "python setup.py develop"
script: pep8 poll --max-line-length=120 && pylint poll && python run_tests.py --with-coverage --cover-package=poll
notifications:
email: false
......@@ -229,3 +229,11 @@ The resulting events look like this for polls:
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.survey.submitted", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Survey"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_survey;_e4975240b6c64a1e988bad86ea917070/handler/vote"}, "time": "2015-01-12T19:13:13.115038+00:00", "ip": "10.0.2.2", "event": {"url_name": "e4975240b6c64a1e988bad86ea917070", "choices": {"enjoy": "Y", "learn": "M", "recommend": "N"}}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.survey.view_results", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Survey"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_survey;_e4975240b6c64a1e988bad86ea917070/handler/get_results"}, "time": "2015-01-12T19:13:13.513909+00:00", "ip": "10.0.2.2", "event": {}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
## Viewing the Results
When running inside the edX LMS, course staff members have the ability to view results without voting.
If you want to grant members of other groups ability to view the results, you can configure the group
names in the django settings using the `XBLOCK_POLL_EXTRA_VIEW_GROUPS` setting, for example:
XBLOCK_POLL_EXTRA_VIEW_GROUPS = ['poll_staff']
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git#egg=xblock-sdk
pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt
pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt
python setup.py develop
......@@ -34,6 +34,16 @@ from xblock.fragment import Fragment
from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader
HAS_EDX_ACCESS = False
try:
# pylint: disable=import-error
from django.conf import settings
from courseware.access import has_access
from api_manager.models import GroupProfile
HAS_EDX_ACCESS = True
except ImportError:
pass
class ResourceMixin(object):
loader = ResourceLoader(__name__)
......@@ -166,6 +176,25 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return True
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_EDX_ACCESS and hasattr(self.runtime, 'user') and hasattr(self.runtime, 'course_id'):
# Course staff users have permission to view results.
if has_access(self.runtime.user, 'staff', self, self.runtime.course_id):
return True
else:
# Check if user is member of a group that is explicitly granted
# permission to view the results through django configuration.
group_names = getattr(settings, 'XBLOCK_POLL_EXTRA_VIEW_GROUPS', [])
if group_names:
group_ids = self.runtime.user.groups.values_list('id', flat=True)
return GroupProfile.objects.filter(group_id__in=group_ids, name__in=group_names).exists()
else:
return False
@staticmethod
def get_max_submissions(data, result, private_results):
"""
......@@ -192,6 +221,8 @@ class PollBlock(PollBase):
Poll XBlock. Allows a teacher to poll users, and presents the results so
far of the poll to the user when finished.
"""
# pylint: disable=too-many-instance-attributes
display_name = String(default='Poll')
question = String(default='What is your favorite color?')
# This will be converted into an OrderedDict.
......@@ -305,6 +336,7 @@ class PollBlock(PollBase):
'can_vote': self.can_vote(),
'max_submissions': self.max_submissions,
'submissions_count': self.submissions_count,
'can_view_private_results': self.can_view_private_results(),
})
if self.choice:
......@@ -346,7 +378,7 @@ class PollBlock(PollBase):
@XBlock.json_handler
def get_results(self, data, suffix=''):
if self.private_results:
if self.private_results and not self.can_view_private_results():
detail, total = {}, None
else:
self.publish_event_from_dict(self.event_namespace + '.view_results', {})
......@@ -448,13 +480,18 @@ class PollBlock(PollBase):
"""
<poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
question="## How long have you been studying with us?"
answers='[["longt", {"label": "A very long time", "img": null}], ["short", {"label": "Not very long", "img": null}], ["not_saying", {"label": "I shall not say", "img": null}], ["longer", {"label": "Longer than you", "img": null}]]'
answers='[["longt", {"label": "A very long time", "img": null}],
["short", {"label": "Not very long", "img": null}],
["not_saying", {"label": "I shall not say", "img": null}],
["longer", {"label": "Longer than you", "img": null}]]'
feedback="### Thank you&#10;&#10;for being a valued student."/>
"""),
]
class SurveyBlock(PollBase):
# pylint: disable=too-many-instance-attributes
display_name = String(default='Survey')
# The display name affects how the block is labeled in the studio,
# but either way we want it to say 'Poll' by default on the page.
......@@ -512,6 +549,7 @@ class SurveyBlock(PollBase):
'can_vote': self.can_vote(),
'submissions_count': self.submissions_count,
'max_submissions': self.max_submissions,
'can_view_private_results': self.can_view_private_results(),
})
return self.create_fragment(
......@@ -554,7 +592,7 @@ class SurveyBlock(PollBase):
tally = []
questions = OrderedDict(self.markdown_items(self.questions))
default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers])
choices = self.choices
choices = self.choices or {}
total = 0
self.clean_tally()
source_tally = self.tally
......@@ -585,7 +623,7 @@ class SurveyBlock(PollBase):
highest = 0
top_index = None
for index, answer in enumerate(question['answers']):
if answer['key'] == choices[question['key']]:
if answer['key'] == choices.get(question['key']):
answer['choice'] = True
# Find the most popular choice.
if answer['count'] > highest:
......@@ -595,7 +633,8 @@ class SurveyBlock(PollBase):
answer['percent'] = round(answer['count'] / float(total) * 100)
except ZeroDivisionError:
answer['percent'] = 0
question['answers'][top_index]['top'] = True
if top_index is not None:
question['answers'][top_index]['top'] = True
return tally, total
......@@ -661,7 +700,7 @@ class SurveyBlock(PollBase):
@XBlock.json_handler
def get_results(self, data, suffix=''):
if self.private_results:
if self.private_results and not self.can_view_private_results():
detail, total = {}, None
else:
self.publish_event_from_dict(self.event_namespace + '.view_results', {})
......@@ -791,9 +830,16 @@ class SurveyBlock(PollBase):
"""),
("Survey Functions",
"""
<survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5}, "q2": {"sa": 3, "a": 2, "n": 3, "d": 10, "sd": 2}, "q3": {"sa": 2, "a": 7, "n": 1, "d": 4, "sd": 6}, "q4": {"sa": 1, "a": 2, "n": 8, "d": 4, "sd": 5}}'
questions='[["q1", {"label": "I feel like this test will pass.", "img": null}], ["q2", {"label": "I like testing software", "img": null}], ["q3", {"label": "Testing is not necessary", "img": null}], ["q4", {"label": "I would fake a test result to get software deployed.", "img": null}]]'
answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"], ["d", "Disagree"], ["sd", "Strongly Disagree"]]'
<survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5},
"q2": {"sa": 3, "a": 2, "n": 3, "d": 10, "sd": 2},
"q3": {"sa": 2, "a": 7, "n": 1, "d": 4, "sd": 6},
"q4": {"sa": 1, "a": 2, "n": 8, "d": 4, "sd": 5}}'
questions='[["q1", {"label": "I feel like this test will pass.", "img": null}],
["q2", {"label": "I like testing software", "img": null}],
["q3", {"label": "Testing is not necessary", "img": null}],
["q4", {"label": "I would fake a test result to get software deployed.", "img": null}]]'
answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"],
["d", "Disagree"], ["sd", "Strongly Disagree"]]'
feedback="### Thank you&#10;&#10;for running the tests."/>
""")
]
......@@ -211,3 +211,8 @@ th.survey-answer {
.poll-submissions-count {
font-weight: bold;
}
.view-results-button-wrapper {
text-align: right;
cursor: pointer;
}
<script id="poll-results-template" type="text/html">
<h3 class="poll-header">{{display_name}}</h3>
<div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results">
<ul class="poll-answers-results poll-results">
{{#each tally}}
<li class="poll-result">
<div class="poll-result-input-container">
......
<script id="survey-results-template" type="text/html">
<h3 class="poll-header">{{block_name}}</h3>
<table class="survey-table">
<table class="survey-table poll-results">
<thead>
<tr>
<th></th>
......
......@@ -26,19 +26,23 @@
</ul>
<input class="input-main" type="button" name="poll-submit" value="{% if choice %}Resubmit{% else %}Submit{% endif %}" disabled />
</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-submissions-count poll-hidden">
You have used <span class="poll-current-count">{{ submissions_count }}</span>
out of <span class="poll-max-submissions">{{ max_submissions }}</span> submissions.
<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">
You have used <span class="poll-current-count">{{ submissions_count }}</span>
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>
{% 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>
{% endif %}
</div>
{% endif %}
{% if can_view_private_results %}
<div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
{% endif %}
{% endif %}
</div>
......@@ -47,5 +47,9 @@
</div>
</div>
{% endif %}
{% if can_view_private_results %}
<div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
{% endif %}
{% endif %}
</div>
......@@ -10,10 +10,12 @@ function PollUtil (runtime, element, pollType) {
this.submit = $('input[type=button]', element);
this.answers = $('input[type=radio]', element);
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
// selected a choice. Render results instead of initializing machinery.
if (! self.submit.length) {
self.getResults({'success': true});
self.onSubmit({'success': true});
return false;
}
var max_submissions = parseInt($('.poll-max-submissions', element).text());
......@@ -42,7 +44,7 @@ function PollUtil (runtime, element, pollType) {
type: "POST",
url: self.voteUrl,
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.
......@@ -73,7 +75,7 @@ function PollUtil (runtime, element, pollType) {
type: "POST",
url: self.voteUrl,
data: JSON.stringify(self.surveyChoices()),
success: self.getResults
success: self.onSubmit
})
});
// If the user has refreshed the page, they may still have an answer
......@@ -86,7 +88,7 @@ function PollUtil (runtime, element, pollType) {
var choices = {};
self.answers.each(function(index, el) {
el = $(el);
choices[el.prop('name')] = $(self.checkedElement(el)).val();
choices[el.prop('name')] = $(self.checkedElement(el), element).val();
});
return choices;
};
......@@ -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.
if (!data['success']) {
alert(data['errors'].join('\n'));
......@@ -135,6 +137,11 @@ function PollUtil (runtime, element, pollType) {
return;
}
// 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({
// Semantically, this would be better as GET, but we can use helper
// functions with POST.
......
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
abstract-class-not-used,
broad-except,
cyclic-import,
fixme,
invalid-name,
locally-disabled,
maybe-no-member,
missing-docstring,
protected-access,
star-args,
too-few-public-methods,
too-many-public-methods,
too-many-ancestors,
too-many-arguments,
too-many-locals,
unused-argument
[SIMILARITIES]
min-similarity-lines=8
......@@ -3,4 +3,4 @@ markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
-e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev
ddt
mock
#!/usr/bin/env python
"""
Run tests for the Poll XBlock
This script is required to run our selenium tests inside the xblock-sdk workbench
because the workbench SDK's settings file is not inside any python module.
"""
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
from django.core.management import execute_from_command_line
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
......@@ -53,6 +53,8 @@ setup(
'XBlock',
'markdown',
'xblock-utils',
'ddt',
'mock',
],
dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
......
......@@ -23,11 +23,29 @@
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from poll.poll import PollBase
from base_test import PollBaseTest, DEFAULT_SURVEY_NAMES, 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
class TestPrivateResults(PollBaseTest):
"""
......@@ -55,7 +73,7 @@ class TestPrivateResults(PollBaseTest):
self.do_submit(names)
# 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')
@unpack
......@@ -83,3 +101,27 @@ class TestPrivateResults(PollBaseTest):
self.assertFalse(self.browser.find_element_by_css_selector('.poll-feedback').is_displayed())
self.do_submit(names)
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