Skip to content

Commit

Permalink
Merge pull request #101 from arbrandes/SOL-1998
Browse files Browse the repository at this point in the history
[SOL-1998] Implement Show Answer button
  • Loading branch information
itsjeyd authored Sep 22, 2016
2 parents 07add13 + 4c6fb7d commit 85c6143
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 63 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ There are two problem modes available:
attempt to place an item, and the number of attempts is not limited.
* **Assessment**: In this mode, the learner places all items on the board and
then clicks a "Submit" button to get feedback. The number of attempts can be
limited.
limited. When all attempts are used, the learner can click a "Show Answer"
button to temporarily place items on their correct drop zones.

![Drop zone edit](/doc/img/edit-view-zones.png)

Expand Down
67 changes: 65 additions & 2 deletions drag_and_drop_v2/drag_and_drop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,28 @@ def reset(self, data, suffix=''):
self.item_state = {}
return self._get_user_state()

@XBlock.json_handler
def show_answer(self, data, suffix=''):
"""
Returns correct answer in assessment mode.
Raises:
* JsonHandlerError with 400 error code in standard mode.
* JsonHandlerError with 409 error code if there are still attempts left
"""
if self.mode != Constants.ASSESSMENT_MODE:
raise JsonHandlerError(
400,
self.i18n_service.gettext("show_answer handler should only be called for assessment mode")
)
if self.attempts_remain:
raise JsonHandlerError(
409,
self.i18n_service.gettext("There are attempts remaining")
)

return self._get_correct_state()

@XBlock.json_handler
def expand_static_url(self, url, suffix=''):
""" AJAX-accessible handler for expanding URLs to static [image] files """
Expand Down Expand Up @@ -527,7 +549,14 @@ def _add_msg_if_exists(ids_list, message_template, message_class):
FeedbackMessages.correctly_placed,
FeedbackMessages.MessageClasses.CORRECTLY_PLACED
)
_add_msg_if_exists(misplaced_ids, FeedbackMessages.misplaced, FeedbackMessages.MessageClasses.MISPLACED)

# Misplaced items are not returned to the bank on the final attempt.
if self.attempts_remain:
misplaced_template = FeedbackMessages.misplaced_returned
else:
misplaced_template = FeedbackMessages.misplaced

_add_msg_if_exists(misplaced_ids, misplaced_template, FeedbackMessages.MessageClasses.MISPLACED)
_add_msg_if_exists(missing_ids, FeedbackMessages.not_placed, FeedbackMessages.MessageClasses.NOT_PLACED)

if self.attempts_remain and (misplaced_ids or missing_ids):
Expand Down Expand Up @@ -723,6 +752,31 @@ def _get_user_state(self):
'overall_feedback': self._present_feedback(overall_feedback_msgs)
}

def _get_correct_state(self):
"""
Returns one of the possible correct states for the configured data.
"""
state = {}
items = copy.deepcopy(self.data.get('items', []))
for item in items:
zones = item.get('zones')

# For backwards compatibility
if zones is None:
zones = []
zone = item.get('zone')
if zone is not None and zone != 'none':
zones.append(zone)

if zones:
zone = zones.pop()
state[str(item['id'])] = {
'zone': zone,
'correct': True,
}

return {'items': state}

def _get_item_state(self):
"""
Returns a copy of the user item state.
Expand Down Expand Up @@ -855,4 +909,13 @@ def workbench_scenarios():
"""
A canned scenario for display in the workbench.
"""
return [("Drag-and-drop-v2 scenario", "<vertical_demo><drag-and-drop-v2/></vertical_demo>")]
return [
(
"Drag-and-drop-v2 standard",
"<vertical_demo><drag-and-drop-v2/></vertical_demo>"
),
(
"Drag-and-drop-v2 assessment",
"<vertical_demo><drag-and-drop-v2 mode='assessment' max_attempts='3'/></vertical_demo>"
),
]
5 changes: 1 addition & 4 deletions drag_and_drop_v2/public/css/drag_and_drop.css
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@
.ltr .xblock--drag-and-drop .actions-toolbar .action-toolbar-item.sidebar-buttons {
float: right;
padding-right: -5px;
padding-top: 5px;
}

.rtl .xblock--drag-and-drop .actions-toolbar .action-toolbar-item.sidebar-buttons {
Expand Down Expand Up @@ -623,10 +624,6 @@
display: block;
}

.xblock--drag-and-drop .reset-button {
margin-top: 3px;
}

/*** ACTIONS TOOLBAR END ***/

/*** KEYBOARD HELP ***/
Expand Down
79 changes: 61 additions & 18 deletions drag_and_drop_v2/public/js/drag_and_drop.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function DragAndDropTemplates(configuration) {
if (item.is_placed) {
var zone_title = (zone.title || "Unknown Zone"); // This "Unknown" text should never be seen, so does not need i18n
var description_content;
if (configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
if (configuration.mode === DragAndDropBlock.ASSESSMENT_MODE && !ctx.showing_answer) {
// In assessment mode placed items will "stick" even when not in correct zone.
description_content = gettext('Placed in: {zone_title}').replace('{zone_title}', zone_title);
} else {
Expand Down Expand Up @@ -180,9 +180,8 @@ function DragAndDropTemplates(configuration) {
var zoneTemplate = function(zone, ctx) {
var className = ctx.display_zone_labels ? 'zone-name' : 'zone-name sr';
var selector = ctx.display_zone_borders ? 'div.zone.zone-with-borders' : 'div.zone';
// If zone is aligned, mark its item alignment
// and render its placed items as children
var item_wrapper = 'div.item-wrapper';
// Mark item alignment and render its placed items as children
var item_wrapper = 'div.item-wrapper.item-align.item-align-' + zone.align;
var is_item_in_zone = function(i) { return i.is_placed && (i.zone === zone.uid); };
var items_in_zone = $.grep(ctx.items, is_item_in_zone);
var zone_description_id = 'zone-' + zone.uid + '-description';
Expand All @@ -199,12 +198,7 @@ function DragAndDropTemplates(configuration) {
gettext('Items placed here: ') + items_in_zone.map(function (item) { return item.displayName; }).join(", ")
);
}
if (zone.align !== 'none') {
item_wrapper += '.item-align.item-align-' + zone.align;
//items_in_zone = $.grep(ctx.items, is_item_in_zone);
} else {
items_in_zone = [];
}

return (
h(
selector,
Expand Down Expand Up @@ -343,14 +337,21 @@ function DragAndDropTemplates(configuration) {
);
};

var sidebarButtonTemplate = function(buttonClass, iconClass, buttonText, disabled) {
var sidebarButtonTemplate = function(buttonClass, iconClass, buttonText, disabled, spinner) {
if (spinner) {
iconClass = 'fa-spin.fa-spinner';
}
return (
h('span.sidebar-button-wrapper', {}, [
h(
'button.unbutton.btn-default.btn-small.'+buttonClass,
{disabled: disabled || false, attributes: {tabindex: 0}},
{disabled: disabled || spinner || false, attributes: {tabindex: 0}},
[
h("span.btn-icon.fa."+iconClass, {attributes: {"aria-hidden": true}}, []),
h(
"span.btn-icon.fa." + iconClass,
{attributes: {"aria-hidden": true}},
[]
),
buttonText
]
)
Expand All @@ -359,10 +360,21 @@ function DragAndDropTemplates(configuration) {
};

var sidebarTemplate = function(ctx) {
var showAnswerButton = null;
if (ctx.show_show_answer) {
showAnswerButton = sidebarButtonTemplate(
"show-answer-button",
"fa-info-circle",
gettext('Show Answer'),
ctx.showing_answer ? true : ctx.disable_show_answer_button,
ctx.show_answer_spinner
);
}
return(
h("section.action-toolbar-item.sidebar-buttons", {}, [
sidebarButtonTemplate("keyboard-help-button", "fa-question", gettext('Keyboard Help')),
sidebarButtonTemplate("reset-button", "fa-refresh", gettext('Reset'), ctx.disable_reset_button),
showAnswerButton,
])
)
};
Expand Down Expand Up @@ -434,9 +446,8 @@ function DragAndDropTemplates(configuration) {
var mainTemplate = function(ctx) {
var problemTitle = ctx.show_title ? h('h3.problem-title', {innerHTML: ctx.title_html}) : null;
var problemHeader = ctx.show_problem_header ? h('h4.title1', gettext('Problem')) : null;

// Render only items_in_bank and items_placed_unaligned here;
// items placed in aligned zones will be rendered by zoneTemplate.
// Render only items in the bank here, including placeholders. Placed
// items will be rendered by zoneTemplate.
var is_item_placed = function(i) { return i.is_placed; };
var items_placed = $.grep(ctx.items, is_item_placed);
var items_in_bank = $.grep(ctx.items, is_item_placed, true);
Expand Down Expand Up @@ -561,6 +572,10 @@ function DragAndDropBlock(runtime, element, configuration) {
$element.on('keydown', '.reset-button', function(evt) {
runOnKey(evt, RET, resetProblem);
});
$element.on('click', '.show-answer-button', showAnswer);
$element.on('keydown', '.show-answer-button', function(evt) {
runOnKey(evt, RET, showAnswer);
});

// For the next one, we need to use addEventListener with useCapture 'true' in order
// to watch for load events on any child element, since load events do not bubble.
Expand Down Expand Up @@ -1098,6 +1113,26 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};

var showAnswer = function(evt) {
evt.preventDefault();
state.show_answer_spinner = true;
applyState();

$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'show_answer'),
data: '{}',
}).done(function(data) {
state.items = data.items;
state.showing_answer = true;
delete state.feedback;
}).always(function() {
state.show_answer_spinner = false;
applyState();
$root.find('.item-bank').focus();
});
};

var doAttempt = function(evt) {
evt.preventDefault();
state.submit_spinner = true;
Expand Down Expand Up @@ -1147,6 +1182,10 @@ function DragAndDropBlock(runtime, element, configuration) {
return any_items_placed && (configuration.mode !== DragAndDropBlock.ASSESSMENT_MODE || attemptsRemain());
};

var canShowAnswer = function() {
return configuration.mode === DragAndDropBlock.ASSESSMENT_MODE && !attemptsRemain();
};

var attemptsRemain = function() {
return !configuration.max_attempts || configuration.max_attempts > state.attempts;
};
Expand Down Expand Up @@ -1207,7 +1246,7 @@ function DragAndDropBlock(runtime, element, configuration) {

// In assessment mode, it is possible to move items back to the bank, so the bank should be able to
// gain focus while keyboard placement is in progress.
var item_bank_focusable = state.keyboard_placement_mode &&
var item_bank_focusable = (state.keyboard_placement_mode || state.showing_answer) &&
configuration.mode === DragAndDropBlock.ASSESSMENT_MODE;

var context = {
Expand All @@ -1220,6 +1259,7 @@ function DragAndDropBlock(runtime, element, configuration) {
problem_html: configuration.problem_text,
show_problem_header: configuration.show_problem_header,
show_submit_answer: configuration.mode == DragAndDropBlock.ASSESSMENT_MODE,
show_show_answer: configuration.mode == DragAndDropBlock.ASSESSMENT_MODE,
target_img_src: configuration.target_img_expanded_url,
target_img_description: configuration.target_img_description,
display_zone_labels: configuration.display_zone_labels,
Expand All @@ -1233,8 +1273,11 @@ function DragAndDropBlock(runtime, element, configuration) {
feedback_messages: state.feedback,
overall_feedback_messages: state.overall_feedback,
disable_reset_button: !canReset(),
disable_show_answer_button: !canShowAnswer(),
disable_submit_button: !canSubmitAttempt(),
submit_spinner: state.submit_spinner
submit_spinner: state.submit_spinner,
showing_answer: state.showing_answer,
show_answer_spinner: state.show_answer_spinner
};

return renderView(context);
Expand Down
24 changes: 23 additions & 1 deletion drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ msgstr ""
msgid "Max number of attempts reached"
msgstr ""

#: drag_and_drop_v2.py
msgid "show_answer handler should only be called for assessment mode"
msgstr ""

#: drag_and_drop_v2.py
msgid "There are attempts remaining"
msgstr ""

#: drag_and_drop_v2.py
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr ""
Expand Down Expand Up @@ -445,6 +453,14 @@ msgstr ""
msgid "Reset"
msgstr ""

#: public/js/drag_and_drop.js
msgid "Show Answer"
msgstr ""

#: public/js/drag_and_drop.js
msgid "Hide Answer"
msgstr ""

#: public/js/drag_and_drop.js
msgid "Submit"
msgstr ""
Expand Down Expand Up @@ -529,7 +545,13 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] ""
msgstr[1] ""

#: utils.py:32
#: utils.py:62
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] ""
msgstr[1] ""

#: utils.py:73
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] ""
Expand Down
28 changes: 25 additions & 3 deletions drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ msgstr "dö_ättémpt händlér shöüld önlý ßé çälléd för ässéssmén
msgid "Max number of attempts reached"
msgstr "Mäx nümßér öf ättémpts réäçhéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"

#: drag_and_drop_v2.py
msgid "show_answer handler should only be called for assessment mode"
msgstr "shöw_änswér händlér shöüld önlý ßé çälléd för ässéssmént mödé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"

#: drag_and_drop_v2.py
msgid "There are attempts remaining"
msgstr "Théré äré ättémpts rémäïnïng Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"

#: drag_and_drop_v2.py
msgid "Unknown DnDv2 mode {mode} - course is misconfigured"
msgstr "Ûnknöwn DnDv2 mödé {mode} - çöürsé ïs mïsçönfïgüréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
Expand Down Expand Up @@ -519,6 +527,14 @@ msgstr "Çörréçtlý pläçéd ïn: {zone_title} Ⱡ'σяєм ιρѕυм ∂σ
msgid "Reset"
msgstr "Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"

#: public/js/drag_and_drop.js
msgid "Show Answer"
msgstr "Shöw Ànswér Ⱡ'σяєм ιρѕυм ∂σłσя #"

#: public/js/drag_and_drop.js
msgid "Hide Answer"
msgstr "Hïdé Ànswér Ⱡ'σяєм ιρѕυм ∂σłσя #"

#: public/js/drag_and_drop.js
msgid "Submit"
msgstr "Süßmït Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
Expand Down Expand Up @@ -617,12 +633,18 @@ msgid_plural "Correctly placed {correct_count} items."
msgstr[0] "Çörréçtlý pläçéd {correct_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
msgstr[1] "Çörréçtlý pläçéd {correct_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"

#: utils.py:32
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
#: utils.py:62
msgid "Misplaced {misplaced_count} item."
msgid_plural "Misplaced {misplaced_count} items."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"

#: utils.py:73
msgid "Misplaced {misplaced_count} item. Misplaced item was returned to item bank."
msgid_plural "Misplaced {misplaced_count} items. Misplaced items were returned to item bank."
msgstr[0] "Mïspläçéd {misplaced_count} ïtém. Mïspläçéd ïtém wäs rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
msgstr[1] "Mïspläçéd {misplaced_count} ïtéms. Mïspläçéd ïtéms wéré rétürnéd tö ïtém ßänk. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"

#: utils.py:40
msgid "Did not place {missing_count} required item."
msgid_plural "Did not place {missing_count} required items."
Expand Down
Loading

0 comments on commit 85c6143

Please sign in to comment.