Skip to content

Commit

Permalink
Allow students to fresh rationales shown
Browse files Browse the repository at this point in the history
- on step 2, display a link to allow students to refresh the answers
shown
  • Loading branch information
kitsook committed Nov 22, 2019
1 parent f80be56 commit 4b0d7ba
Show file tree
Hide file tree
Showing 11 changed files with 468 additions and 15 deletions.
91 changes: 91 additions & 0 deletions ubcpi/answer_pool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import random
import copy
import persistence as sas_api
from utils import _ # pylint: disable=unused-import

Expand Down Expand Up @@ -323,6 +324,96 @@ def get_other_answers_random(pool, seeded_answers, get_student_item_dict, num_re
return {"answers": ret}


def refresh_answers(answers_shown, option, pool, seeded_answers, get_student_item_dict, seeded_first=False):
"""
Refresh the answers shown for given option
Args:
answers_shown (dict): answers being shown that need to be refreshed. Format:
{'answers': [
{'option': 0, 'rationale': 'rationale A'},
{'option': 1, 'rationale': 'rationale B'},
]}
option (int): the option to refresh
pool (dict): answer pool, format:
{
option1_index: {
student_id: { can store algorithm specific info here }
},
option2_index: {
student_id: { ... }
}
}
seeded_answers (list): seeded answers from instructor
[
{'answer': 0, 'rationale': 'rationale A'},
{'answer': 1, 'rationale': 'rationale B'},
]
get_student_item_dict (callable): get student item dict function to return student item dict
seeded_first (boolean): refresh with answers from seeded_answers first, when exhausted, pick from pool
Returns:
dict: refreshed answers lists
{
'answers':
[
{'option': 0, 'rationale': 'rationale A'},
{'option': 1, 'rationale': 'rationale B'},
]
}
"""
ret = copy.deepcopy(answers_shown)
# clean up answers so that all keys are int
pool = {int(k): v for k, v in pool.items()}
seeded_pool = convert_seeded_answers(seeded_answers)
student_id = get_student_item_dict()['student_id']

available_students = copy.deepcopy(pool.get(option, {}))
available_students.pop(student_id, None)
# if seed answers have higher priority, fill the available seeds.
# otherwise merge them into available students
available_seeds = {}
if seeded_first and seeded_pool.get(option, {}):
available_seeds = copy.deepcopy(seeded_pool.get(option, {}))
else:
for key in seeded_pool.get(option, {}):
available_students[key] = seeded_pool.get(option, {}).get(key, None)

for answer in ret.get('answers', []):
if answer.get('option', None) == option:
rationale = None

while available_seeds:
key = random.choice(available_seeds.keys())
rationale = available_seeds.pop(key, None)
if rationale is not None:
answer['rationale'] = rationale
break;

while available_students and rationale is None:
key = random.choice(available_students.keys())
# remove the chosen answer from pool
content = available_students.pop(key, None)

if key.startswith('seeded'):
rationale = content
else:
student_item = get_student_item_dict(key)
submission = sas_api.get_answers_for_student(student_item)
# Make sure the answer is still the one we want.
# It may have changed (e.g. instructor deleted the student state
# and the student re-submitted a diff answer)
if submission.has_revision(0) and submission.get_vote(0) == option:
rationale = submission.get_rationale(0)

if rationale:
answer['rationale'] = rationale
break

# random.shuffle(ret['answers'])
return ret


def convert_seeded_answers(answers):
"""
Convert seeded answers into the format that can be merged into student answers.
Expand Down
17 changes: 13 additions & 4 deletions ubcpi/static/css/ubcpi.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fieldset .ubcpi-label {
border-radius: 3px;
padding: 10px;
width: 100%;
background-color: #fbfbfb;
}

fieldset .ubcpi-label:hover {
Expand Down Expand Up @@ -191,10 +192,6 @@ div.course-wrapper section.course-content .vert-mod > div ul.ubcpi-other-answers
border-bottom: 1px solid #cfc6c6;
}

div.course-wrapper section.course-content .vert-mod .sample-answer-list > :not(:first-child) {
border-top: 1px solid #cfc6c6;
}

div.course-wrapper section.course-content .vert-mod .sample-answer {
margin: 1em;
display: block;
Expand Down Expand Up @@ -631,6 +628,18 @@ text.ubcpibar.label {
display: block;
}

.ubcpi-refresh-section {
margin: 1em;
border-top: 1px solid #cfc6c6;
}

.ubcpi-refresh-option-button * {
font-size: 0.9em;
color: #0075b4;
transition: none;
cursor: pointer;
}

#pi-form .list-input.settings-list .setting-label {
vertical-align: top;
}
Expand Down
12 changes: 8 additions & 4 deletions ubcpi/static/html/ubcpi.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ <h3 id="pi-question-h" class="question-text" style="display:inline;">{{display_n
<legend>
<span data-ng-if="rc.status() == rc.ALL_STATUS.NEW" style="color:#414141" translate>Step 1) Give Initial Answer <span class="inline-hint">You can change this answer later, if you change your mind.</span></span>
<span data-ng-if="rc.status() == rc.ALL_STATUS.ANSWERED" style="color:#414141" translate>Step 2) Read Other Student Answers
<p class="ubcpi-other-answers-instructions" translate>These are samples of other student answers for this question. Read them and then compare with your answer below.</p>
<p class="ubcpi-other-answers-instructions" translate>These are randomly chosen samples of other student answers for this question. Read them and compare with your answer below. Then you may revise your answer, if you wish.</p>
</span>
</legend>

<div id="hiding-options-div" class="ubcpi-possible-options">
<div class="ubcpi-option" data-ng-repeat="(optionKey, option) in options track by $index">
<label class="ubcpi-label ubcpi-answer" data-ng-class="{'ubcpi-no-pointer': rc.status() == rc.ALL_STATUS.ANSWERED && !rc.revising}" for="original-option-input-{{ $index }}">
<label class="ubcpi-label ubcpi-answer" data-ng-class="{'ubcpi-no-pointer': rc.status() == rc.ALL_STATUS.ANSWERED && !rc.revising}">
<input class="ubcpi-field" type="radio" id="original-option-input-{{ $index }}" data-ng-if="rc.status() == rc.ALL_STATUS.NEW || rc.revising" name="q" data-ng-model="rc.answer" value="{{optionKey}}" required integer>

<img data-ng-src="{{option.image_url}}" id="original-option-image-{{ $index }}" alt="{{option.image_alt}}" data-ng-if="option.image_position == 'above' && option.image_url" />
Expand All @@ -69,6 +69,9 @@ <h3 id="pi-question-h" class="question-text" style="display:inline;">{{display_n
<span class="other-rationale">"{{otherAnswer.rationale}}"</span>
</div>
</div>
<div class="ubcpi-refresh-section" data-ng-if="rc.status() == rc.ALL_STATUS.ANSWERED && !rc.revising">
<span class="ubcpi-refresh-option-button" ubcpi-refresh-rationale ubcpi-option="{{optionKey}}" ubcpi-refresh-model="rc.other_answers.answers" translate></span>
</div>
</div>
<div class="no-sample-answer" ng-if="rc.status() == rc.ALL_STATUS.ANSWERED && !rc.hasSampleExplanationForOption(optionKey)">
(no student explanations were randomly selected for this answer)
Expand Down Expand Up @@ -110,9 +113,10 @@ <h3 id="pi-question-h" class="question-text" style="display:inline;">{{display_n
</div>
<div style="text-align: right;">
<p id="decision-prompt" style="text-align: right;" data-ng-if="!rc.revising" translate>What would you like to do?</p>
<input id="dummy-button" style="margin-right: 5px; display: inline-block; display:none;" type='button' class='ubcpi_submit' name='ubcpi_dummy' />
<input id="update-button" style="margin-right: 5px; display: inline-block;" type='button' class='ubcpi_submit' data-ng-if="!rc.revising" value="{{ 'Revise Answer' | translate }}" name='ubcpi_update_step' data-ng-click="rc.revising=true;"/>
<input id="cancel-button" style="margin-right: 5px; display: inline-block;" type='button' class='ubcpi_submit' data-ng-if="rc.revising" value="{{ 'Cancel' | translate }}" name='ubcpi_update_step' data-ng-click="rc.answer=rc.answer_original; rc.rationale=rc.rationale_original; rc.revising=false;"/>
<input id="submit-button" style="display: inline-block;" onclick="this.blur(); " style="display: inline; margin-left: 5px;" data-ng-disabled="answerForm.$invalid" type='button' class='ubcpi_submit' value="{{ 'Submit Answer' | translate }}" name='ubcpi_next_step' data-ng-click="rc.clickSubmit($event);" aria-describedby="button-disabled-reason ubcpi-next-inline-hints"/>
<input id="cancel-button" style="margin-right: 5px; display: inline-block;" type='button' class='ubcpi_submit' data-ng-if="rc.revising" value="{{ 'Cancel' | translate }}" name='ubcpi_update_step_cancel' data-ng-click="rc.answer=rc.answer_original; rc.rationale=rc.rationale_original; rc.revising=false;"/>
<input id="submit-button" style="display: inline-block;" onclick="this.blur(); " style="display: inline; margin-left: 5px;" data-ng-disabled="answerForm.$invalid" type='button' class='ubcpi_submit' value="{{ 'Submit Answer' | translate }}" name='ubcpi_next_step' data-ng-click="rc.clickSubmit($event);" aria-describedby="button-disabled-reason ubcpi-next-inline-hints" scroll-to-top-of-block='click' onclick=""/>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ubcpi/static/html/ubcpi_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
<div class="wrapper-comp-setting pi-wrapper-comp-setting">
<div class="pi-label-and-hint">
<label class="label setting-label pi-setting-label" for="pi-algo-num-responses" aria-describedby="pi-num-responses-tip" translate>Answers Students See - Number Selected <font color="red">*</font></label>
<span id="pi-num-responses-tip" class="tip setting-help" translate>This is the number of examples shown to the students after they answer. Enter the # symbol to use the same number as the answer possibilities you've set.</span>
<span id="pi-num-responses-tip" class="tip setting-help" translate>This is the number of examples shown on screen to the students after they answer. Students can choose to refresh for some other samples. Enter the # symbol to use the same number as the answer possibilities you've set.</span>
</div>
<input class="input setting-input pi-options" name="pi-algo-num-responses" id="pi-algo-num-responses" ng-model="esc.data.algo.num_responses" ng-model-options="{ debounce: 500 }" type="text" required />
</div>
Expand Down
99 changes: 97 additions & 2 deletions ubcpi/static/js/spec/ubcpi_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,69 @@ describe('UBCPI module', function () {
});
});

describe('ubcpi-refresh-rationale directive', function() {
var $compile,
$rootScope,
backendService,
$httpBackend;

// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function (_$compile_, _$rootScope_, _$httpBackend_) {
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;

mockConfig.urls.refresh_other_answers = '/handler/refresh_other_answers';
}));

afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});

it('should show button to refresh answers shown', function () {
var scope = $rootScope.$new(true);
scope.rc = {
other_answers: { answer: [] }
};
// Compile a piece of HTML containing the directive
var element = $compile("<div name=\"test_div\" class=\"ubcpi-refresh-option-button\" ubcpi-refresh-rationale ubcpi-option=\"1\" ubcpi-refresh-model=\"rc.other_answers.answers\"></div>")(scope);
scope.$digest();
expect(element.html()).toContain('Show other samples');
});

it('should allow refreshing of answers shown', function () {
var scope = $rootScope.$new(true);
scope.rc = {
other_answers: { answers: [] }
};
var param = {"option": "1"};
var exp = {
"other_answers": {
"answers": [
{"option": 0, "rationale": "Tree gets carbon from air."},
{"option": 1, "rationale": "Tree gets minerals from soil."},
{"option": 2, "rationale": "Tree drinks water."}]
},
"rationale_revised": null,
"answer_original": 1,
"rationale_original": "testing",
"answer_revised": null
};
var data = undefined;
$httpBackend.expectPOST('/handler/refresh_other_answers', JSON.stringify(param)).respond(200, exp);

// Compile a piece of HTML containing the directive
var element = $compile("<div name=\"test_div\" class=\"ubcpi-refresh-option-button\" ubcpi-refresh-rationale ubcpi-option=\"1\" ubcpi-refresh-model=\"rc.other_answers.answers\"></div>")(scope);
scope.$digest();
$(element).click();
$httpBackend.flush();
expect(scope.rc.other_answers).toEqual(exp.other_answers);
});
});

describe('backendService', function() {
var backendService, $httpBackend;

Expand Down Expand Up @@ -128,6 +191,38 @@ describe('UBCPI module', function () {
});
});

describe('refresh_other_answers', function() {

beforeEach(function() {
mockConfig.urls.refresh_other_answers = '/handler/refresh_other_answers';
});

it('should be able to refresh other answers', function() {
var param = {"option": 1};
var exp = {
"other_answers": {
"answers": [
{"option": 0, "rationale": "Tree gets carbon from air."},
{"option": 1, "rationale": "Tree gets minerals from soil."},
{"option": 2, "rationale": "Tree drinks water."}]
},
"rationale_revised": null,
"answer_original": 1,
"rationale_original": "testing",
"answer_revised": null
};
var data = undefined;
$httpBackend.expectPOST('/handler/refresh_other_answers', JSON.stringify(param)).respond(200, exp);

backendService.refreshOtherAnswers(1).then(function(d) {
data = d;
});
$httpBackend.flush();

expect(data).toEqual(exp);
});
});

describe('submit', function() {
var post = {
"q": 0,
Expand Down Expand Up @@ -450,8 +545,8 @@ describe('PeerInstructionXBlock function', function() {
});

it('should generate URLs using runtime', function() {
expect(mockRuntime.handlerUrl.calls.count()).toBe(4);
expect(mockRuntime.handlerUrl.calls.count()).toBe(5);
expect(mockRuntime.handlerUrl.calls.allArgs()).toEqual(
[[mockElement, 'get_stats'], [mockElement, 'submit_answer'], [mockElement, 'get_asset'], [mockElement, 'get_data']]);
[[mockElement, 'get_stats'], [mockElement, 'submit_answer'], [mockElement, 'get_asset'], [mockElement, 'get_data'], [mockElement, 'refresh_other_answers']]);
});
});
3 changes: 2 additions & 1 deletion ubcpi/static/js/src/d3-pibar.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,14 @@ d3.custom.perAnswerChart = function(scope, gettext, allAnswerCount) {
});

if (totalFreq < minTotalFrequency) {
d3.select(this).selectAll("svg > *").remove();
d3.select(this)
.append("span")
.attr("id", 'not-enough-data')
.text(gettext("Not enough data to generate the chart. Please check back later."));
return;
} else {
var notEnoughDataSpan = d3.select('#not-enough-data');
var notEnoughDataSpan = d3.select(this).select('#not-enough-data');
if (typeof notEnoughDataSpan !== 'undefined') {
notEnoughDataSpan.remove();
}
Expand Down

0 comments on commit 4b0d7ba

Please sign in to comment.