Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-poll
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
xblock-poll
Commits
d5d995d6
Commit
d5d995d6
authored
Feb 13, 2015
by
Jonathan Piacenti
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added 'max submissions' functionality.
parent
09e8feda
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
209 additions
and
32 deletions
+209
-32
poll/poll.py
+68
-4
poll/public/html/poll.html
+2
-2
poll/public/html/poll_edit.html
+12
-1
poll/public/html/survey.html
+3
-3
poll/public/js/poll.js
+12
-2
poll/public/js/poll_edit.js
+1
-0
tests/integration/base_test.py
+21
-0
tests/integration/test_max_submissions.py
+82
-0
tests/integration/test_private_results.py
+2
-18
tests/integration/xml/poll_max_submissions.xml
+1
-0
tests/integration/xml/poll_max_submissions_infinite.xml
+1
-0
tests/integration/xml/poll_private.xml
+1
-1
tests/integration/xml/survey_max_submissions.xml
+1
-0
tests/integration/xml/survey_max_submissions_infinite.xml
+1
-0
tests/integration/xml/survey_private.xml
+1
-1
No files found.
poll/poll.py
View file @
d5d995d6
...
...
@@ -29,7 +29,7 @@ from markdown import markdown
import
pkg_resources
from
xblock.core
import
XBlock
from
xblock.fields
import
Scope
,
String
,
Dict
,
List
,
Boolean
from
xblock.fields
import
Scope
,
String
,
Dict
,
List
,
Boolean
,
Integer
from
xblock.fragment
import
Fragment
from
xblockutils.publish_event
import
PublishEventMixin
from
xblockutils.resources
import
ResourceLoader
...
...
@@ -63,6 +63,10 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
"""
event_namespace
=
'xblock.pollbase'
private_results
=
Boolean
(
default
=
False
,
help
=
"Whether or not to display results to the user."
)
max_submissions
=
Integer
(
default
=
1
,
help
=
"The maximum number of times a user may send a submission."
)
submissions_count
=
Integer
(
default
=
0
,
help
=
"Number of times the user has sent a submission."
,
scope
=
Scope
.
user_state
)
feedback
=
String
(
default
=
''
,
help
=
"Text to display after the user votes."
)
def
send_vote_event
(
self
,
choice_data
):
...
...
@@ -152,6 +156,36 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return
items
def
can_vote
(
self
):
"""
Checks to see if the user is permitted to vote. This may not be the case if they used up their max_submissions.
"""
if
self
.
max_submissions
==
0
:
return
True
if
self
.
max_submissions
>
self
.
submissions_count
:
return
True
return
False
@staticmethod
def
get_max_submissions
(
data
,
result
,
private_results
):
"""
Gets the value of 'max_submissions' from studio submitted AJAX data, and checks for conflicts
with private_results, which may not be False when max_submissions is not 1, since that would mean
the student could change their answer based on other students' answers.
"""
try
:
max_submissions
=
int
(
data
[
'max_submissions'
])
except
(
ValueError
,
KeyError
):
max_submissions
=
1
result
[
'success'
]
=
False
result
[
'errors'
]
.
append
(
'Maximum Submissions missing or not an integer.'
)
# Better to send an error than to confuse the user by thinking this would work.
if
(
max_submissions
!=
1
)
and
not
private_results
:
result
[
'success'
]
=
False
result
[
'errors'
]
.
append
(
"Private results may not be False when Maximum Submissions is not 1."
)
return
max_submissions
class
PollBlock
(
PollBase
):
"""
...
...
@@ -267,7 +301,8 @@ class PollBlock(PollBase):
'any_img'
:
self
.
any_image
(
self
.
answers
),
# The SDK doesn't set url_name.
'url_name'
:
getattr
(
self
,
'url_name'
,
''
),
"display_name"
:
self
.
display_name
,
'display_name'
:
self
.
display_name
,
'can_vote'
:
self
.
can_vote
(),
})
if
self
.
choice
:
...
...
@@ -288,7 +323,8 @@ class PollBlock(PollBase):
'display_name'
:
self
.
display_name
,
'private_results'
:
self
.
private_results
,
'feedback'
:
self
.
feedback
,
'js_template'
:
js_template
'js_template'
:
js_template
,
'max_submissions'
:
self
.
max_submissions
,
})
return
self
.
create_fragment
(
context
,
"public/html/poll_edit.html"
,
...
...
@@ -341,13 +377,22 @@ class PollBlock(PollBase):
result
[
'errors'
]
.
append
(
'No key "{choice}" in answers table.'
.
format
(
choice
=
choice
))
return
result
if
old_choice
is
None
:
# Reset submissions count if old choice is bogus.
self
.
submissions_count
=
0
if
not
self
.
can_vote
():
result
[
'errors'
]
.
append
(
'You have already voted as many times as you are allowed.'
)
self
.
clean_tally
()
if
old_choice
is
not
None
:
self
.
tally
[
old_choice
]
-=
1
self
.
choice
=
choice
self
.
tally
[
choice
]
+=
1
self
.
submissions_count
+=
1
result
[
'success'
]
=
True
result
[
'can_vote'
]
=
self
.
can_vote
()
self
.
send_vote_event
({
'choice'
:
self
.
choice
})
...
...
@@ -359,6 +404,9 @@ class PollBlock(PollBase):
question
=
data
.
get
(
'question'
,
''
)
.
strip
()
feedback
=
data
.
get
(
'feedback'
,
''
)
.
strip
()
private_results
=
bool
(
data
.
get
(
'private_results'
,
False
))
max_submissions
=
self
.
get_max_submissions
(
data
,
result
,
private_results
)
display_name
=
data
.
get
(
'display_name'
,
''
)
.
strip
()
if
not
question
:
result
[
'errors'
]
.
append
(
"You must specify a question."
)
...
...
@@ -374,6 +422,7 @@ class PollBlock(PollBase):
self
.
feedback
=
feedback
self
.
private_results
=
private_results
self
.
display_name
=
display_name
self
.
max_submissions
=
max_submissions
# Tally will not be updated until the next attempt to use it, per
# scoping limitations.
...
...
@@ -454,7 +503,8 @@ class SurveyBlock(PollBase):
'feedback'
:
markdown
(
self
.
feedback
)
or
False
,
# The SDK doesn't set url_name.
'url_name'
:
getattr
(
self
,
'url_name'
,
''
),
"block_name"
:
self
.
block_name
,
'block_name'
:
self
.
block_name
,
'can_vote'
:
self
.
can_vote
()
})
return
self
.
create_fragment
(
...
...
@@ -482,6 +532,7 @@ class SurveyBlock(PollBase):
'display_name'
:
self
.
block_name
,
'private_results'
:
self
.
private_results
,
'js_template'
:
js_template
,
'max_submissions'
:
self
.
max_submissions
,
'multiquestion'
:
True
,
})
return
self
.
create_fragment
(
...
...
@@ -649,6 +700,14 @@ class SurveyBlock(PollBase):
result
[
'success'
]
=
False
result
[
'errors'
]
.
append
(
"You have already voted in this poll."
)
if
not
choices
:
# Reset submissions count if choices are bogus.
self
.
submissions_count
=
0
if
not
self
.
can_vote
():
result
[
'success'
]
=
False
result
[
'errors'
]
.
append
(
'You have already voted as many times as you are allowed.'
)
# Make sure the user has included all questions, and hasn't included
# anything extra, which might indicate the questions have changed.
if
not
sorted
(
data
.
keys
())
==
sorted
(
questions
.
keys
()):
...
...
@@ -666,6 +725,7 @@ class SurveyBlock(PollBase):
"Found unknown answer '
%
s' for question key '
%
s'"
%
(
key
,
value
))
if
not
result
[
'success'
]:
result
[
'can_vote'
]
=
self
.
can_vote
()
return
result
# Record the vote!
...
...
@@ -675,8 +735,10 @@ class SurveyBlock(PollBase):
self
.
clean_tally
()
for
key
,
value
in
self
.
choices
.
items
():
self
.
tally
[
key
][
value
]
+=
1
self
.
submissions_count
+=
1
self
.
send_vote_event
({
'choices'
:
self
.
choices
})
result
[
'can_vote'
]
=
self
.
can_vote
()
return
result
...
...
@@ -688,6 +750,7 @@ class SurveyBlock(PollBase):
feedback
=
data
.
get
(
'feedback'
,
''
)
.
strip
()
block_name
=
data
.
get
(
'display_name'
,
''
)
.
strip
()
private_results
=
bool
(
data
.
get
(
'private_results'
,
False
))
max_submissions
=
self
.
get_max_submissions
(
data
,
result
,
private_results
)
answers
=
self
.
gather_items
(
data
,
result
,
'Answer'
,
'answers'
,
image
=
False
)
questions
=
self
.
gather_items
(
data
,
result
,
'Question'
,
'questions'
)
...
...
@@ -699,6 +762,7 @@ class SurveyBlock(PollBase):
self
.
questions
=
questions
self
.
feedback
=
feedback
self
.
private_results
=
private_results
self
.
max_submissions
=
max_submissions
self
.
block_name
=
block_name
# Tally will not be updated until the next attempt to use it, per
...
...
poll/public/html/poll.html
View file @
d5d995d6
{{ js_template|safe }}
<div
class=
"poll-block"
data-private=
"{% if private_results %}1{% endif %}"
>
<div
class=
"poll-block"
data-private=
"{% if private_results %}1{% endif %}"
data-can-vote=
"{% if can_vote %}1{% endif %}"
>
{# If no form is present, the Javascript will load the results instead. #}
{% if private_results or not choice %}
<h3
class=
"poll-header"
>
{{display_name}}
</h3>
...
...
@@ -26,7 +26,7 @@
</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 %} 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>
{% if feedback %}
<div
class=
"poll-feedback-container{% if not choice %} poll-hidden{% endif %}"
>
<hr
/>
...
...
poll/public/html/poll_edit.html
View file @
d5d995d6
...
...
@@ -4,7 +4,7 @@
<ul
class=
"list-input settings-list"
id=
"poll-line-items"
>
<li
class=
"field comp-setting-entry is-set"
>
<div
class=
"wrapper-comp-setting"
>
<label
class=
"label setting-label poll-setting-label"
for=
"
display_
name"
>
Display Name
</label>
<label
class=
"label setting-label poll-setting-label"
for=
"
poll-display-
name"
>
Display Name
</label>
<!-- In the case of surveys, this field will actually be used for block_name. -->
<input
class=
"input setting-input"
name=
"display_name"
id=
"poll-display-name"
value=
"{{ display_name }}"
type=
"text"
/>
</div>
...
...
@@ -44,6 +44,17 @@
</span>
</li>
<li
class=
"field comp-setting-entry is-set"
>
<div
class=
"wrapper-comp-setting"
>
<label
class=
"label setting-label poll-setting-label"
for=
"poll-max-submissions"
>
Maximum Submissions
</label>
<input
id=
"poll-max-submissions"
type=
"number"
min=
"0"
step=
"1"
value=
"{{ max_submissions }}"
/>
</div>
<span
class=
"tip setting-help"
>
Maximum number of times a user may submit a poll. **Setting this to a value other than 1 will imply that
'Private Results' should be true.** Setting it to 0 will allow infinite submissions.
resubmissions.
</span>
</li>
<li
class=
"field comp-setting-entry is-set"
>
<p>
<strong>
Notes:
</strong>
If you change an answer's text, all students who voted for that choice will have their votes updated to
...
...
poll/public/html/survey.html
View file @
d5d995d6
{{ js_template|safe }}
<div
class=
"poll-block"
data-private=
"{% if private_results %}1{% endif %}"
>
<div
class=
"poll-block"
data-private=
"{% if private_results %}1{% endif %}"
data-can-vote=
"{% if can_vote %}1{% endif %}"
>
{# If no form is present, the Javascript will load the results instead. #}
{% if not choices or private_results %}
<h3
class=
"poll-header"
>
{{block_name}}
</h3>
...
...
@@ -31,9 +31,9 @@
</tr>
{% endfor %}
</table>
<input
class=
"input-main"
type=
"button"
name=
"poll-submit"
value=
"{% if choices %}Resubmit{% else %}Submit{% endif %}"
disabled
/>
<input
class=
"input-main"
type=
"button"
name=
"poll-submit"
value=
"{% if choices
and can_vote
%}Resubmit{% else %}Submit{% endif %}"
disabled
/>
</form>
<div
class=
"poll-voting-thanks{% if not choices %} poll-hidden{% endif %}"
><span>
Thank you for your submission!
</span></div>
<div
class=
"poll-voting-thanks{% if not choices
or can_vote
%} poll-hidden{% endif %}"
><span>
Thank you for your submission!
</span></div>
{% if feedback %}
<div
class=
"poll-feedback-container{% if not choices %} poll-hidden{% endif %}"
>
<hr
/>
...
...
poll/public/js/poll.js
View file @
d5d995d6
...
...
@@ -36,6 +36,10 @@ function PollUtil (runtime, element, pollType) {
success
:
self
.
getResults
});
});
// If the user has already reached their maximum submissions, all inputs should be disabled.
if
(
!
$
(
'div.poll-block'
,
element
).
data
(
'can-vote'
))
{
$
(
'input'
,
element
).
attr
(
'disabled'
,
true
);
}
// If the user has refreshed the page, they may still have an answer
// selected and the submit button should be enabled.
var
answers
=
$
(
'input[type=radio]'
,
element
);
...
...
@@ -97,15 +101,21 @@ function PollUtil (runtime, element, pollType) {
if
(
!
data
[
'success'
])
{
alert
(
data
[
'errors'
].
join
(
'
\
n'
));
}
if
(
$
(
'div.poll-block'
,
element
).
attr
(
'data-private'
))
{
var
can_vote
=
data
[
'can_vote'
];
if
(
$
(
'div.poll-block'
,
element
).
data
(
'private'
))
{
// User may be changing their vote. Give visual feedback that it was accepted.
var
thanks
=
$
(
'.poll-voting-thanks'
,
element
);
thanks
.
removeClass
(
'poll-hidden'
);
thanks
.
fadeOut
(
0
).
fadeIn
(
'slow'
,
'swing'
);
$
(
'.poll-feedback-container'
,
element
).
removeClass
(
'poll-hidden'
);
$
(
'input[name="poll-submit"]'
,
element
).
val
(
'Resubmit'
);
if
(
can_vote
)
{
$
(
'input[name="poll-submit"]'
,
element
).
val
(
'Resubmit'
);
}
else
{
$
(
'input'
,
element
).
attr
(
'disabled'
,
true
)
}
return
;
}
// 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.
...
...
poll/public/js/poll_edit.js
View file @
d5d995d6
...
...
@@ -222,6 +222,7 @@ function PollEditUtil(runtime, element, pollType) {
data
[
'display_name'
]
=
$
(
'#poll-display-name'
,
element
).
val
();
data
[
'question'
]
=
$
(
'#poll-question-editor'
,
element
).
val
();
data
[
'feedback'
]
=
$
(
'#poll-feedback-editor'
,
element
).
val
();
data
[
'max_submissions'
]
=
$
(
'#poll-max-submissions'
,
element
).
val
();
// Convert to boolean for transfer.
data
[
'private_results'
]
=
eval
(
$
(
'#poll-private-results'
,
element
).
val
());
...
...
tests/integration/base_test.py
View file @
d5d995d6
...
...
@@ -24,9 +24,30 @@
from
xblockutils.base_test
import
SeleniumBaseTest
# Default names for inputs for polls/surveys
DEFAULT_SURVEY_NAMES
=
(
'enjoy'
,
'recommend'
,
'learn'
)
DEFAULT_POLL_NAMES
=
(
'choice'
,)
class
PollBaseTest
(
SeleniumBaseTest
):
default_css_selector
=
'div.poll-block'
module_name
=
__name__
def
get_submit
(
self
):
return
self
.
browser
.
find_element_by_css_selector
(
'input[name="poll-submit"]'
)
def
make_selections
(
self
,
names
):
"""
Selects the first option for each named input.
"""
for
name
in
names
:
self
.
browser
.
find_element_by_css_selector
(
'input[name="
%
s"]'
%
name
)
.
click
()
def
do_submit
(
self
,
names
):
"""
Do selection and submit.
"""
self
.
make_selections
(
names
)
submit
=
self
.
get_submit
()
submit
.
click
()
self
.
wait_until_clickable
(
self
.
browser
.
find_element_by_css_selector
(
'.poll-voting-thanks'
))
tests/integration/test_max_submissions.py
0 → 100644
View file @
d5d995d6
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 McKinsey Academy
#
# Authors:
# Jonathan Piacenti <jonathan@opencraft.com>
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
from
ddt
import
ddt
,
unpack
,
data
from
tests.integration.base_test
import
PollBaseTest
,
DEFAULT_POLL_NAMES
,
DEFAULT_SURVEY_NAMES
scenarios_infinite
=
(
(
'Survey Max Submissions Infinite'
,
DEFAULT_SURVEY_NAMES
),
(
'Poll Max Submissions Infinite'
,
DEFAULT_POLL_NAMES
),
)
scenarios_max
=
(
(
'Survey Max Submissions'
,
DEFAULT_SURVEY_NAMES
),
(
'Poll Max Submissions'
,
DEFAULT_POLL_NAMES
),
)
@ddt
class
TestPrivateResults
(
PollBaseTest
):
@unpack
@data
(
*
scenarios_infinite
)
def
test_infinite_submissions
(
self
,
page
,
names
):
"""
We can't actually test infinite submissions, but we can be reasonably certain it will work
if it has worked a few times more than we have allocated, which should be '0' according to the
setting, which is actually code for 'as often as you like' rather than '0 attempts permitted'.
Try this by staying on the page, and by loading it up again.
"""
for
__
in
range
(
0
,
2
):
self
.
go_to_page
(
page
)
for
___
in
range
(
1
,
5
):
self
.
submission_run
(
names
)
self
.
assertTrue
(
self
.
get_submit
()
.
is_enabled
())
def
submission_run
(
self
,
names
):
self
.
do_submit
(
names
)
self
.
browser
.
execute_script
(
"$('.poll-voting-thanks').stop().addClass('poll-hidden').removeAttr('style')"
)
self
.
wait_until_hidden
(
self
.
browser
.
find_element_by_css_selector
(
'.poll-voting-thanks'
))
@unpack
@data
(
*
scenarios_max
)
def
test_max_submissions_one_view
(
self
,
page
,
names
):
"""
Verify that the user can't submit more than a certain number of times. Our XML allows two submissions.
"""
self
.
go_to_page
(
page
)
for
__
in
range
(
0
,
2
):
self
.
do_submit
(
names
)
self
.
assertFalse
(
self
.
get_submit
()
.
is_enabled
())
@unpack
@data
(
*
scenarios_max
)
def
test_max_submissions_reload
(
self
,
page
,
names
):
"""
Same as above, but revisit the page between attempts.
"""
self
.
go_to_page
(
page
)
self
.
do_submit
(
names
)
self
.
go_to_page
(
page
)
self
.
do_submit
(
names
)
self
.
assertFalse
(
self
.
get_submit
()
.
is_enabled
())
tests/integration/test_private_results.py
View file @
d5d995d6
...
...
@@ -23,10 +23,10 @@
from
ddt
import
ddt
,
unpack
,
data
from
selenium.common.exceptions
import
NoSuchElementException
from
base_test
import
PollBaseTest
from
base_test
import
PollBaseTest
,
DEFAULT_SURVEY_NAMES
,
DEFAULT_POLL_NAMES
scenarios
=
(
'Survey Private'
,
[
'enjoy'
,
'recommend'
,
'learn'
]),
(
'Poll Private'
,
[
'choice'
]
)
scenarios
=
(
'Survey Private'
,
DEFAULT_SURVEY_NAMES
),
(
'Poll Private'
,
DEFAULT_POLL_NAMES
)
@ddt
class
TestPrivateResults
(
PollBaseTest
):
...
...
@@ -34,22 +34,6 @@ class TestPrivateResults(PollBaseTest):
Check the functionality of private results.
"""
def
make_selections
(
self
,
names
):
"""
Selects the first option for each named input.
"""
for
name
in
names
:
self
.
browser
.
find_element_by_css_selector
(
'input[name="
%
s"]'
%
name
)
.
click
()
def
do_submit
(
self
,
names
):
"""
Do selection and submit.
"""
self
.
make_selections
(
names
)
submit
=
self
.
get_submit
()
submit
.
click
()
self
.
wait_until_clickable
(
self
.
browser
.
find_element_by_css_selector
(
'.poll-voting-thanks'
))
@unpack
@data
(
*
scenarios
)
def
test_form_remains
(
self
,
page_name
,
names
):
...
...
tests/integration/xml/poll_max_submissions.xml
0 → 100644
View file @
d5d995d6
<poll
private_results=
"true"
max_submissions=
"2"
feedback=
"### Thank you for being a valued student."
/>
tests/integration/xml/poll_max_submissions_infinite.xml
0 → 100644
View file @
d5d995d6
<poll
private_results=
"true"
max_submissions=
"0"
feedback=
"### Thank you for being a valued student."
/>
tests/integration/xml/poll_private.xml
View file @
d5d995d6
<poll
private_results=
"true"
feedback=
"### Thank you for being a valued student."
/>
<poll
private_results=
"true"
max_submissions=
"4"
feedback=
"### Thank you for being a valued student."
/>
tests/integration/xml/survey_max_submissions.xml
0 → 100644
View file @
d5d995d6
<survey
url_name=
"defaults"
private_results=
"true"
max_submissions=
"2"
/>
tests/integration/xml/survey_max_submissions_infinite.xml
0 → 100644
View file @
d5d995d6
<survey
url_name=
"defaults"
private_results=
"true"
max_submissions=
"0"
/>
tests/integration/xml/survey_private.xml
View file @
d5d995d6
<survey
private_results=
"true"
feedback=
"### Thank you for running the tests."
/>
<survey
private_results=
"true"
max_submissions=
"4"
feedback=
"### Thank you for running the tests."
/>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment