Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SOL-1998] Implement Show Answer button #101

Merged
merged 5 commits into from
Sep 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -502,6 +502,7 @@
float: right;
margin: 0;
padding-right: -5px;
padding-top: 5px;
}
}

Expand Down Expand Up @@ -536,10 +537,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