-
-
+
{word_count_message}
+
-
+
{submitted_message}
-
- {word_count_message}
-
-
-
-
+
+
{used_attempts_feedback}
+
+ {user_alert}
+
diff --git a/freetextresponse/private/view.js b/freetextresponse/private/view.js
index e086f34b..915d9d73 100644
--- a/freetextresponse/private/view.js
+++ b/freetextresponse/private/view.js
@@ -4,14 +4,28 @@ function FreeTextResponseView(runtime, element) {
var $ = window.jQuery;
var $element = $(element);
var buttonSubmit = $element.find('.check.Submit');
- var textareaParent = $element.find('.student_answer').parent();
+ var buttonSave = $element.find('.save');
var usedAttemptsFeedback = $element.find('.action .used-attempts-feedback');
var problemProgress = $element.find('.problem-progress');
- var submitParent = $element.find('.Submit').parent();
- var wordCountError = $element.find('.word-count-error');
var submissionReceivedMessage = $element.find('.submission-received');
+ var userAlertMessage = $element.find('.user_alert');
+ var textareaStudentAnswer = $element.find('.student_answer');
+ var textareaParent = textareaStudentAnswer.parent();
+
var url = runtime.handlerUrl(element, 'submit');
- var userInputClass = 'user-input';
+ var urlSave = runtime.handlerUrl(element, 'save_reponse');
+
+ // POLYFILL notify if it does not exist. Like in the xblock workbench.
+ runtime.notify = runtime.notify || function () {
+ console.log('POLYFILL runtime.notify', arguments);
+ };
+
+ function setClassForTextAreaParent(new_class) {
+ textareaParent.removeClass('correct');
+ textareaParent.removeClass('incorrect');
+ textareaParent.removeClass('unanswered');
+ textareaParent.addClass(new_class);
+ }
buttonSubmit.on('click', function () {
buttonSubmit.text('Checking...');
@@ -25,17 +39,15 @@ function FreeTextResponseView(runtime, element) {
'student_answer': $element.find('.student_answer').val()
}),
success: function buttonSubmitOnSuccess(response) {
- textareaParent.removeClass();
- textareaParent.addClass(userInputClass);
- textareaParent.addClass(response.indicator_class);
usedAttemptsFeedback.text(response.used_attempts_feedback);
- submitParent.removeClass();
- submitParent.addClass(response.submit_class);
+ buttonSubmit.addClass(response.nodisplay_class);
problemProgress.text(response.problem_progress);
- wordCountError.text(response.word_count_message);
submissionReceivedMessage.text(response.submitted_message);
buttonSubmit.text('Submit');
-
+ userAlertMessage.text(response.user_alert);
+ buttonSave.addClass(response.nodisplay_class);
+ setClassForTextAreaParent(response.indicator_class);
+
runtime.notify('submit', {
state: 'end'
});
@@ -46,4 +58,42 @@ function FreeTextResponseView(runtime, element) {
});
return false;
});
+
+ buttonSave.on('click', function () {
+ buttonSave.text('Checking...');
+ runtime.notify('save', {
+ message: 'Saving...',
+ state: 'start'
+ });
+ $.ajax(urlSave, {
+ type: 'POST',
+ data: JSON.stringify({
+ 'student_answer': $element.find('.student_answer').val()
+ }),
+ success: function buttonSaveOnSuccess(response) {
+ buttonSubmit.addClass(response.nodisplay_class);
+ buttonSave.addClass(response.nodisplay_class);
+ usedAttemptsFeedback.text(response.used_attempts_feedback);
+ problemProgress.text(response.problem_progress);
+ submissionReceivedMessage.text(response.submitted_message);
+ buttonSave.text('Save');
+ userAlertMessage.text(response.user_alert);
+
+ runtime.notify('save', {
+ state: 'end'
+ });
+ },
+ error: function buttonSaveOnError() {
+ runtime.notify('error', {});
+ }
+ });
+ return false;
+ });
+
+ textareaStudentAnswer.on('keydown', function() {
+ // Reset Messages
+ submissionReceivedMessage.text('');
+ userAlertMessage.text('');
+ setClassForTextAreaParent('unanswered');
+ });
}
diff --git a/freetextresponse/private/view.less b/freetextresponse/private/view.less
index cf24f5fa..7d9ff34f 100644
--- a/freetextresponse/private/view.less
+++ b/freetextresponse/private/view.less
@@ -1,8 +1,8 @@
.freetextresponse {
- .user-input {
+ .user_input {
- .status{
+ .status {
margin-left: 10px;
display: inline-block;
}
@@ -24,6 +24,27 @@
height: 20px;
background: url("images/incorrect-icon.png") center center no-repeat;
}
+
+ .student_answer {
+ height:150px;
+ box-sizing:border-box;
+ border-radius:3px;
+ border:2px solid #e4e4e4;
+ min-width:160px;
+ }
+
+ &.unanswered .student_answer {
+ border:2px solid #e4e4e4;
+ }
+
+ &.correct .student_answer {
+ border:2px solid #1e9348;
+ }
+
+ &.incorrect .student_answer {
+ border:2px solid #b20610;
+ }
+
}
@dark_grey: #666;
@@ -31,6 +52,12 @@
.action {
margin-top: 20px;
+ .save {
+ height: 40px;
+ vertical-align: middle;
+ font-weight: 600;
+ }
+
.check {
height: 40px;
vertical-align: middle;
@@ -58,6 +85,14 @@
font-size: 1em;
}
+ .word-count-message {
+ display: inline-block;
+ padding-left: 5px;
+ color: @dark_grey;
+ font-weight: 100;
+ font-size: 1em;
+ }
+
.hidden{
visibility: hidden;
}
@@ -66,11 +101,14 @@
display: inline-block;
}
- .word-count-error {
- color: #A50000;
+ .capa_alert {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border: 1px solid #ebe8bf;
+ border-radius: 3px;
+ background: #fffcdd;
+ font-size: 0.9em;
}
- .submission-received {
- color: #2EA41E;
- }
+ div:empty { display: none }
}
diff --git a/freetextresponse/public/view.html b/freetextresponse/public/view.html
index 36f2ddbd..dfcab702 100644
--- a/freetextresponse/public/view.html
+++ b/freetextresponse/public/view.html
@@ -1 +1 @@
-
{problem_progress}
{self.prompt}
{submitted_message}
{word_count_message}
{used_attempts_feedback}
\ No newline at end of file
+
{problem_progress}
{self.prompt}
{word_count_message}
{submitted_message}
{used_attempts_feedback}
{user_alert}
\ No newline at end of file
diff --git a/freetextresponse/public/view.js b/freetextresponse/public/view.js
index e086f34b..915d9d73 100644
--- a/freetextresponse/public/view.js
+++ b/freetextresponse/public/view.js
@@ -4,14 +4,28 @@ function FreeTextResponseView(runtime, element) {
var $ = window.jQuery;
var $element = $(element);
var buttonSubmit = $element.find('.check.Submit');
- var textareaParent = $element.find('.student_answer').parent();
+ var buttonSave = $element.find('.save');
var usedAttemptsFeedback = $element.find('.action .used-attempts-feedback');
var problemProgress = $element.find('.problem-progress');
- var submitParent = $element.find('.Submit').parent();
- var wordCountError = $element.find('.word-count-error');
var submissionReceivedMessage = $element.find('.submission-received');
+ var userAlertMessage = $element.find('.user_alert');
+ var textareaStudentAnswer = $element.find('.student_answer');
+ var textareaParent = textareaStudentAnswer.parent();
+
var url = runtime.handlerUrl(element, 'submit');
- var userInputClass = 'user-input';
+ var urlSave = runtime.handlerUrl(element, 'save_reponse');
+
+ // POLYFILL notify if it does not exist. Like in the xblock workbench.
+ runtime.notify = runtime.notify || function () {
+ console.log('POLYFILL runtime.notify', arguments);
+ };
+
+ function setClassForTextAreaParent(new_class) {
+ textareaParent.removeClass('correct');
+ textareaParent.removeClass('incorrect');
+ textareaParent.removeClass('unanswered');
+ textareaParent.addClass(new_class);
+ }
buttonSubmit.on('click', function () {
buttonSubmit.text('Checking...');
@@ -25,17 +39,15 @@ function FreeTextResponseView(runtime, element) {
'student_answer': $element.find('.student_answer').val()
}),
success: function buttonSubmitOnSuccess(response) {
- textareaParent.removeClass();
- textareaParent.addClass(userInputClass);
- textareaParent.addClass(response.indicator_class);
usedAttemptsFeedback.text(response.used_attempts_feedback);
- submitParent.removeClass();
- submitParent.addClass(response.submit_class);
+ buttonSubmit.addClass(response.nodisplay_class);
problemProgress.text(response.problem_progress);
- wordCountError.text(response.word_count_message);
submissionReceivedMessage.text(response.submitted_message);
buttonSubmit.text('Submit');
-
+ userAlertMessage.text(response.user_alert);
+ buttonSave.addClass(response.nodisplay_class);
+ setClassForTextAreaParent(response.indicator_class);
+
runtime.notify('submit', {
state: 'end'
});
@@ -46,4 +58,42 @@ function FreeTextResponseView(runtime, element) {
});
return false;
});
+
+ buttonSave.on('click', function () {
+ buttonSave.text('Checking...');
+ runtime.notify('save', {
+ message: 'Saving...',
+ state: 'start'
+ });
+ $.ajax(urlSave, {
+ type: 'POST',
+ data: JSON.stringify({
+ 'student_answer': $element.find('.student_answer').val()
+ }),
+ success: function buttonSaveOnSuccess(response) {
+ buttonSubmit.addClass(response.nodisplay_class);
+ buttonSave.addClass(response.nodisplay_class);
+ usedAttemptsFeedback.text(response.used_attempts_feedback);
+ problemProgress.text(response.problem_progress);
+ submissionReceivedMessage.text(response.submitted_message);
+ buttonSave.text('Save');
+ userAlertMessage.text(response.user_alert);
+
+ runtime.notify('save', {
+ state: 'end'
+ });
+ },
+ error: function buttonSaveOnError() {
+ runtime.notify('error', {});
+ }
+ });
+ return false;
+ });
+
+ textareaStudentAnswer.on('keydown', function() {
+ // Reset Messages
+ submissionReceivedMessage.text('');
+ userAlertMessage.text('');
+ setClassForTextAreaParent('unanswered');
+ });
}
diff --git a/freetextresponse/public/view.js.min.js b/freetextresponse/public/view.js.min.js
index ab44d9ad..bc45d02d 100644
--- a/freetextresponse/public/view.js.min.js
+++ b/freetextresponse/public/view.js.min.js
@@ -1,2 +1,2 @@
-function FreeTextResponseView(a,b){"use strict";var c=window.jQuery,d=c(b),e=d.find(".check.Submit"),f=d.find(".student_answer").parent(),g=d.find(".action .used-attempts-feedback"),h=d.find(".problem-progress"),i=d.find(".Submit").parent(),j=d.find(".word-count-error"),k=d.find(".submission-received"),l=a.handlerUrl(b,"submit"),m="user-input";e.on("click",function(){return e.text("Checking..."),a.notify("submit",{message:"Submitting...",state:"start"}),c.ajax(l,{type:"POST",data:JSON.stringify({student_answer:d.find(".student_answer").val()}),success:function(b){f.removeClass(),f.addClass(m),f.addClass(b.indicator_class),g.text(b.used_attempts_feedback),i.removeClass(),i.addClass(b.submit_class),h.text(b.problem_progress),j.text(b.word_count_message),k.text(b.submitted_message),e.text("Submit"),a.notify("submit",{state:"end"})},error:function(){a.notify("error",{})}}),!1})}
+function FreeTextResponseView(a,b){"use strict";function c(a){m.removeClass("correct"),m.removeClass("incorrect"),m.removeClass("unanswered"),m.addClass(a)}var d=window.jQuery,e=d(b),f=e.find(".check.Submit"),g=e.find(".save"),h=e.find(".action .used-attempts-feedback"),i=e.find(".problem-progress"),j=e.find(".submission-received"),k=e.find(".user_alert"),l=e.find(".student_answer"),m=l.parent(),n=a.handlerUrl(b,"submit"),o=a.handlerUrl(b,"save_reponse");a.notify=a.notify||function(){console.log("POLYFILL runtime.notify",arguments)},f.on("click",function(){return f.text("Checking..."),a.notify("submit",{message:"Submitting...",state:"start"}),d.ajax(n,{type:"POST",data:JSON.stringify({student_answer:e.find(".student_answer").val()}),success:function(b){h.text(b.used_attempts_feedback),f.addClass(b.nodisplay_class),i.text(b.problem_progress),j.text(b.submitted_message),f.text("Submit"),k.text(b.user_alert),g.addClass(b.nodisplay_class),c(b.indicator_class),a.notify("submit",{state:"end"})},error:function(){a.notify("error",{})}}),!1}),g.on("click",function(){return g.text("Checking..."),a.notify("save",{message:"Saving...",state:"start"}),d.ajax(o,{type:"POST",data:JSON.stringify({student_answer:e.find(".student_answer").val()}),success:function(b){f.addClass(b.nodisplay_class),g.addClass(b.nodisplay_class),h.text(b.used_attempts_feedback),i.text(b.problem_progress),j.text(b.submitted_message),g.text("Save"),k.text(b.user_alert),a.notify("save",{state:"end"})},error:function(){a.notify("error",{})}}),!1}),l.on("keydown",function(){j.text(""),k.text(""),c("unanswered")})}
//# sourceMappingURL=view.js.min.js.map
diff --git a/freetextresponse/public/view.js.min.js.map b/freetextresponse/public/view.js.min.js.map
index b098ba50..776e08ac 100644
--- a/freetextresponse/public/view.js.min.js.map
+++ b/freetextresponse/public/view.js.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["view.js"],"names":["FreeTextResponseView","runtime","element","$","window","jQuery","$element","buttonSubmit","find","textareaParent","parent","usedAttemptsFeedback","problemProgress","submitParent","wordCountError","submissionReceivedMessage","url","handlerUrl","userInputClass","on","text","notify","message","state","ajax","type","data","JSON","stringify","student_answer","val","success","response","removeClass","addClass","indicator_class","used_attempts_feedback","submit_class","problem_progress","word_count_message","submitted_message","error"],"mappings":"AAAA,QAASA,sBAAqBC,EAASC,GACnC,YAEA,IAAIC,GAAIC,OAAOC,OACXC,EAAWH,EAAED,GACbK,EAAeD,EAASE,KAAK,iBAC7BC,EAAiBH,EAASE,KAAK,mBAAmBE,SAClDC,EAAuBL,EAASE,KAAK,mCACrCI,EAAkBN,EAASE,KAAK,qBAChCK,EAAeP,EAASE,KAAK,WAAWE,SACxCI,EAAiBR,EAASE,KAAK,qBAC/BO,EAA4BT,EAASE,KAAK,wBAC1CQ,EAAMf,EAAQgB,WAAWf,EAAS,UAClCgB,EAAiB,YAErBX,GAAaY,GAAG,QAAS,WA+BrB,MA9BAZ,GAAaa,KAAK,eAClBnB,EAAQoB,OAAO,UACXC,QAAS,gBACTC,MAAO,UAEXpB,EAAEqB,KAAKR,GACHS,KAAM,OACNC,KAAMC,KAAKC,WACPC,eAAkBvB,EAASE,KAAK,mBAAmBsB,QAEvDC,QAAS,SAA+BC,GACpCvB,EAAewB,cACfxB,EAAeyB,SAAShB,GACxBT,EAAeyB,SAASF,EAASG,iBACjCxB,EAAqBS,KAAKY,EAASI,wBACnCvB,EAAaoB,cACbpB,EAAaqB,SAASF,EAASK,cAC/BzB,EAAgBQ,KAAKY,EAASM,kBAC9BxB,EAAeM,KAAKY,EAASO,oBAC7BxB,EAA0BK,KAAKY,EAASQ,mBACxCjC,EAAaa,KAAK,UAElBnB,EAAQoB,OAAO,UACXE,MAAO,SAGfkB,MAAO,WACHxC,EAAQoB,OAAO,gBAGhB","file":"view.js.min.js"}
\ No newline at end of file
+{"version":3,"file":"view.js.min.js","sources":["view.js"],"names":["FreeTextResponseView","runtime","element","setClassForTextAreaParent","new_class","textareaParent","removeClass","addClass","$","window","jQuery","$element","buttonSubmit","find","buttonSave","usedAttemptsFeedback","problemProgress","submissionReceivedMessage","userAlertMessage","textareaStudentAnswer","parent","url","handlerUrl","urlSave","notify","console","log","arguments","on","text","message","state","ajax","type","data","JSON","stringify","student_answer","val","success","response","used_attempts_feedback","nodisplay_class","problem_progress","submitted_message","user_alert","indicator_class","error"],"mappings":"AAAA,QAASA,sBAAqBC,EAASC,GACnC,YAqBA,SAASC,GAA0BC,GAC/BC,EAAeC,YAAY,WAC3BD,EAAeC,YAAY,aAC3BD,EAAeC,YAAY,cAC3BD,EAAeE,SAASH,GAvB5B,GAAII,GAAIC,OAAOC,OACXC,EAAWH,EAAEN,GACbU,EAAeD,EAASE,KAAK,iBAC7BC,EAAaH,EAASE,KAAK,SAC3BE,EAAuBJ,EAASE,KAAK,mCACrCG,EAAkBL,EAASE,KAAK,qBAChCI,EAA4BN,EAASE,KAAK,wBAC1CK,EAAmBP,EAASE,KAAK,eACjCM,EAAwBR,EAASE,KAAK,mBACtCR,EAAiBc,EAAsBC,SAEvCC,EAAMpB,EAAQqB,WAAWpB,EAAS,UAClCqB,EAAUtB,EAAQqB,WAAWpB,EAAS,eAG1CD,GAAQuB,OAASvB,EAAQuB,QAAU,WAC/BC,QAAQC,IAAI,0BAA2BC,YAU3Cf,EAAagB,GAAG,QAAS,WA6BrB,MA5BAhB,GAAaiB,KAAK,eAClB5B,EAAQuB,OAAO,UACXM,QAAS,gBACTC,MAAO,UAEXvB,EAAEwB,KAAKX,GACHY,KAAM,OACNC,KAAMC,KAAKC,WACPC,eAAkB1B,EAASE,KAAK,mBAAmByB,QAEvDC,QAAS,SAA+BC,GACpCzB,EAAqBc,KAAKW,EAASC,wBACnC7B,EAAaL,SAASiC,EAASE,iBAC/B1B,EAAgBa,KAAKW,EAASG,kBAC9B1B,EAA0BY,KAAKW,EAASI,mBACxChC,EAAaiB,KAAK,UAClBX,EAAiBW,KAAKW,EAASK,YAC/B/B,EAAWP,SAASiC,EAASE,iBAC7BvC,EAA0BqC,EAASM,iBAEnC7C,EAAQuB,OAAO,UACXO,MAAO,SAGfgB,MAAO,WACH9C,EAAQuB,OAAO,gBAGhB,IAGXV,EAAWc,GAAG,QAAS,WA4BnB,MA3BAd,GAAWe,KAAK,eAChB5B,EAAQuB,OAAO,QACXM,QAAS,YACTC,MAAO,UAEXvB,EAAEwB,KAAKT,GACHU,KAAM,OACNC,KAAMC,KAAKC,WACPC,eAAkB1B,EAASE,KAAK,mBAAmByB,QAEvDC,QAAS,SAA6BC,GAClC5B,EAAaL,SAASiC,EAASE,iBAC/B5B,EAAWP,SAASiC,EAASE,iBAC7B3B,EAAqBc,KAAKW,EAASC,wBACnCzB,EAAgBa,KAAKW,EAASG,kBAC9B1B,EAA0BY,KAAKW,EAASI,mBACxC9B,EAAWe,KAAK,QAChBX,EAAiBW,KAAKW,EAASK,YAE/B5C,EAAQuB,OAAO,QACXO,MAAO,SAGfgB,MAAO,WACH9C,EAAQuB,OAAO,gBAGhB,IAGXL,EAAsBS,GAAG,UAAW,WAEhCX,EAA0BY,KAAK,IAC/BX,EAAiBW,KAAK,IACtB1B,EAA0B"}
\ No newline at end of file
diff --git a/freetextresponse/public/view.less b/freetextresponse/public/view.less
index cf24f5fa..bc9d8924 100644
--- a/freetextresponse/public/view.less
+++ b/freetextresponse/public/view.less
@@ -1,8 +1,9 @@
.freetextresponse {
- .user-input {
- .status{
+ .user_input {
+
+ .status {
margin-left: 10px;
display: inline-block;
}
@@ -24,6 +25,27 @@
height: 20px;
background: url("images/incorrect-icon.png") center center no-repeat;
}
+
+ .student_answer {
+ height:150px;
+ box-sizing:border-box;
+ border-radius:3px;
+ border:2px solid #e4e4e4;
+ min-width:160px;
+ }
+
+ &.unanswered .student_answer {
+ border:2px solid #e4e4e4;
+ }
+
+ &.correct .student_answer {
+ border:2px solid #1e9348;
+ }
+
+ &.incorrect .student_answer {
+ border:2px solid #b20610;
+ }
+
}
@dark_grey: #666;
@@ -31,6 +53,12 @@
.action {
margin-top: 20px;
+ .save {
+ height: 40px;
+ vertical-align: middle;
+ font-weight: 600;
+ }
+
.check {
height: 40px;
vertical-align: middle;
@@ -58,6 +86,14 @@
font-size: 1em;
}
+ .word-count-message {
+ display: inline-block;
+ padding-left: 5px;
+ color: @dark_grey;
+ font-weight: 100;
+ font-size: 1em;
+ }
+
.hidden{
visibility: hidden;
}
@@ -66,11 +102,14 @@
display: inline-block;
}
- .word-count-error {
- color: #A50000;
+ .capa_alert {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border: 1px solid #ebe8bf;
+ border-radius: 3px;
+ background: #fffcdd;
+ font-size: 0.9em;
}
- .submission-received {
- color: #2EA41E;
- }
+ div:empty { display: none }
}
diff --git a/freetextresponse/public/view.less.min.css b/freetextresponse/public/view.less.min.css
index 59c24e26..5d61c453 100644
--- a/freetextresponse/public/view.less.min.css
+++ b/freetextresponse/public/view.less.min.css
@@ -1 +1 @@
-.freetextresponse .user-input .status{margin-left:10px;display:inline-block}.freetextresponse .user-input.unanswered .status{width:14px;height:14px;background:url(images/unanswered-icon.png) center center no-repeat}.freetextresponse .user-input.correct .status{width:25px;height:20px;background:url(images/correct-icon.png) center center no-repeat}.freetextresponse .user-input.incorrect .status{width:20px;height:20px;background:url(images/incorrect-icon.png) center center no-repeat}.freetextresponse .action{margin-top:20px}.freetextresponse .action .check{height:40px;vertical-align:middle;font-weight:600}.freetextresponse .action .used-attempts-feedback{display:inline-block;margin-top:8px;margin-left:10px;color:#666;font-style:italic}.freetextresponse .action .nodisplay{display:none}.freetextresponse .problem-progress{display:inline-block;padding-left:5px;color:#666;font-weight:100;font-size:1em}.freetextresponse .hidden{visibility:hidden}.freetextresponse .problem-header{display:inline-block}.freetextresponse .word-count-error{color:#A50000}.freetextresponse .submission-received{color:#2EA41E}
\ No newline at end of file
+.freetextresponse .user_input .status{margin-left:10px;display:inline-block}.freetextresponse .user_input.unanswered .status{width:14px;height:14px;background:url(images/unanswered-icon.png) center center no-repeat}.freetextresponse .user_input.correct .status{width:25px;height:20px;background:url(images/correct-icon.png) center center no-repeat}.freetextresponse .user_input.incorrect .status{width:20px;height:20px;background:url(images/incorrect-icon.png) center center no-repeat}.freetextresponse .user_input .student_answer{height:150px;box-sizing:border-box;border-radius:3px;border:2px solid #e4e4e4;min-width:160px}.freetextresponse .user_input.unanswered .student_answer{border:2px solid #e4e4e4}.freetextresponse .user_input.correct .student_answer{border:2px solid #1e9348}.freetextresponse .user_input.incorrect .student_answer{border:2px solid #b20610}.freetextresponse .action{margin-top:20px}.freetextresponse .action .save{height:40px;vertical-align:middle;font-weight:600}.freetextresponse .action .check{height:40px;vertical-align:middle;font-weight:600}.freetextresponse .action .used-attempts-feedback{display:inline-block;margin-top:8px;margin-left:10px;color:#666;font-style:italic}.freetextresponse .action .nodisplay{display:none}.freetextresponse .problem-progress{display:inline-block;padding-left:5px;color:#666;font-weight:100;font-size:1em}.freetextresponse .word-count-message{display:inline-block;padding-left:5px;color:#666;font-weight:100;font-size:1em}.freetextresponse .hidden{visibility:hidden}.freetextresponse .problem-header{display:inline-block}.freetextresponse .capa_alert{margin-top:10px;padding:8px 12px;border:1px solid #ebe8bf;border-radius:3px;background:#fffcdd;font-size:.9em}.freetextresponse div:empty{display:none}
\ No newline at end of file
diff --git a/freetextresponse/public/view.less.min.css.map b/freetextresponse/public/view.less.min.css.map
index 2e4823cf..8891cb3c 100644
--- a/freetextresponse/public/view.less.min.css.map
+++ b/freetextresponse/public/view.less.min.css.map
@@ -1 +1 @@
-{"version":3,"sources":["./freetextresponse/public/view.less"],"names":[],"mappings":"AAAA,iBAEE,YAEE,SACE,gBAAA,CACA,qBAGF,iBAPF,YAOG,WAAY,SACX,UAAA,CACA,WAAA,CACA,eAAgB,sDAGlB,iBAbF,YAaG,QAAS,SACR,UAAA,CACA,WAAA,CACA,eAAgB,mDAGlB,iBAnBF,YAmBG,UAAW,SACV,UAAA,CACA,WAAA,CACA,eAAgB,qDAxBtB,iBA8BE,SACE,gBA/BJ,iBA8BE,QAGE,QACE,WAAA,CACA,qBAAA,CACA,gBApCN,iBA8BE,QASE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBA5CN,iBA8BE,QAiBE,YACE,aAhDN,iBAoDE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cAzDJ,iBA4DE,SACE,kBA7DJ,iBAgEE,iBACE,qBAjEJ,iBAoEE,mBACE,cArEJ,iBAwEE,sBACE","sourcesContent":[".freetextresponse {\n\n .user-input {\n\n .status{\n margin-left: 10px;\n display: inline-block;\n }\n\n &.unanswered .status {\n width: 14px;\n height: 14px;\n background: url(\"images/unanswered-icon.png\") center center no-repeat;\n }\n\n &.correct .status {\n width: 25px;\n height: 20px;\n background: url(\"images/correct-icon.png\") center center no-repeat;\n }\n\n &.incorrect .status {\n width: 20px;\n height: 20px;\n background: url(\"images/incorrect-icon.png\") center center no-repeat;\n }\n }\n\n @dark_grey: #666;\n\n .action {\n margin-top: 20px;\n\n .check {\n height: 40px;\n vertical-align: middle;\n font-weight: 600;\n }\n\n .used-attempts-feedback {\n display: inline-block;\n margin-top: 8px;\n margin-left: 10px;\n color: @dark_grey;\n font-style: italic;\n }\n\n .nodisplay{\n display: none;\n }\n }\n\n .problem-progress {\n display: inline-block;\n padding-left: 5px;\n color: @dark_grey;\n font-weight: 100;\n font-size: 1em;\n }\n\n .hidden{\n visibility: hidden;\n }\n\n .problem-header {\n display: inline-block;\n }\n\n .word-count-error {\n color: #A50000;\n }\n\n .submission-received {\n color: #2EA41E;\n }\n}\n"]}
\ No newline at end of file
+{"version":3,"sources":["./freetextresponse/public/view.less"],"names":[],"mappings":"AAAA,iBAGE,YAEE,SACE,gBAAA,CACA,qBAGF,iBAPF,YAOG,WAAY,SACX,UAAA,CACA,WAAA,CACA,eAAgB,sDAGlB,iBAbF,YAaG,QAAS,SACR,UAAA,CACA,WAAA,CACA,eAAgB,mDAGlB,iBAnBF,YAmBG,UAAW,SACV,UAAA,CACA,WAAA,CACA,eAAgB,qDAzBtB,iBAGE,YAyBE,iBACE,YAAA,CACA,qBAAA,CACA,iBAAA,CACA,wBAAA,CACA,gBAGF,iBAjCF,YAiCG,WAAY,iBACX,yBAGF,iBArCF,YAqCG,QAAS,iBACR,yBAGF,iBAzCF,YAyCG,UAAW,iBACV,yBA7CN,iBAoDE,SACE,gBArDJ,iBAoDE,QAGE,OACE,WAAA,CACA,qBAAA,CACA,gBA1DN,iBAoDE,QASE,QACE,WAAA,CACA,qBAAA,CACA,gBAhEN,iBAoDE,QAeE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBAxEN,iBAoDE,QAuBE,YACE,aA5EN,iBAgFE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cArFJ,iBAwFE,qBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cA7FJ,iBAgGE,SACE,kBAjGJ,iBAoGE,iBACE,qBArGJ,iBAwGE,aACE,eAAA,CACA,gBAAA,CACA,wBAAA,CACA,iBAAA,CACA,kBAAA,CACA,gBA9GJ,iBAiHE,IAAG,OAAS","sourcesContent":[".freetextresponse {\n\n\n .user_input {\n\n .status {\n margin-left: 10px;\n display: inline-block;\n }\n\n &.unanswered .status {\n width: 14px;\n height: 14px;\n background: url(\"images/unanswered-icon.png\") center center no-repeat;\n }\n\n &.correct .status {\n width: 25px;\n height: 20px;\n background: url(\"images/correct-icon.png\") center center no-repeat;\n }\n\n &.incorrect .status {\n width: 20px;\n height: 20px;\n background: url(\"images/incorrect-icon.png\") center center no-repeat;\n }\n\n .student_answer {\n height:150px;\n box-sizing:border-box;\n border-radius:3px;\n border:2px solid #e4e4e4;\n min-width:160px;\n }\n\n &.unanswered .student_answer {\n border:2px solid #e4e4e4;\n }\n\n &.correct .student_answer {\n border:2px solid #1e9348;\n }\n\n &.incorrect .student_answer {\n border:2px solid #b20610;\n }\n\n }\n\n @dark_grey: #666;\n\n .action {\n margin-top: 20px;\n\n .save {\n height: 40px;\n vertical-align: middle;\n font-weight: 600;\n }\n\n .check {\n height: 40px;\n vertical-align: middle;\n font-weight: 600;\n }\n\n .used-attempts-feedback {\n display: inline-block;\n margin-top: 8px;\n margin-left: 10px;\n color: @dark_grey;\n font-style: italic;\n }\n\n .nodisplay{\n display: none;\n }\n }\n\n .problem-progress {\n display: inline-block;\n padding-left: 5px;\n color: @dark_grey;\n font-weight: 100;\n font-size: 1em;\n }\n\n .word-count-message {\n display: inline-block;\n padding-left: 5px;\n color: @dark_grey;\n font-weight: 100;\n font-size: 1em;\n }\n\n .hidden{\n visibility: hidden;\n }\n\n .problem-header {\n display: inline-block;\n }\n\n .capa_alert {\n margin-top: 10px;\n padding: 8px 12px;\n border: 1px solid #ebe8bf;\n border-radius: 3px;\n background: #fffcdd;\n font-size: 0.9em;\n }\n\n div:empty { display: none }\n}\n"]}
\ No newline at end of file
diff --git a/freetextresponse/tests.py b/freetextresponse/tests.py
index 04abd2ab..7238de85 100644
--- a/freetextresponse/tests.py
+++ b/freetextresponse/tests.py
@@ -1,26 +1,52 @@
"""
-Module Placeholder Docstring
+Module To Test FreeTextResponse XBlock
"""
+import json
import unittest
-
-from django.test.client import Client
-from django.utils.translation import ugettext as _
+import ddt
from mock import MagicMock, Mock
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xblock.field_data import DictFieldData
+from xblock.validation import ValidationMessage
from .freetextresponse import Credit
from .freetextresponse import FreeTextResponse
+from .utils import _
+
+
+class TestData(object):
+ # pylint: disable=too-few-public-methods
+ """
+ Module helper for validate_field_data
+ """
+ weight = 0
+ max_attempts = 0
+ max_word_count = 0
+ min_word_count = 0
+ submitted_message = None
+
+
+class TestRequest(object):
+ # pylint: disable=too-few-public-methods
+ """
+ Module helper for @json_handler
+ """
+ method = None
+ body = None
+ success = None
+
+@ddt.ddt
class FreetextResponseXblockTestCase(unittest.TestCase):
# pylint: disable=too-many-instance-attributes, too-many-public-methods
"""
A complete suite of unit tests for the Free-text Response XBlock
"""
+
@classmethod
def make_an_xblock(cls, **kw):
"""
@@ -35,61 +61,163 @@ def make_an_xblock(cls, **kw):
return xblock
def setUp(self):
+ """
+ Creates an xblock
+ """
self.xblock = FreetextResponseXblockTestCase.make_an_xblock()
- self.client = Client()
- self.test_display_name = 'test_display_name',
- self.test_prompt = 'test_prompt'
- self.test_weight = 5
- self.test_max_attempts = 10
- self.test_display_correctness = 'True'
- self.test_min_word_count = 5
- self.test_max_word_count = 10
- self.test_fullcredit_keyphrases = 'test fullcredit phrase 1, ' \
- 'test fullcredit phrase 2'
- self.test_halfcredit_keyphrases = 'test halfcredit phrase 1, ' \
- 'test halfcredit phrase 2'
- self.test_student_answer = 'test student answer'
- self.test_count_attempts = '3'
- self.test_submitted_message = 'test submission received message'
- def test_student_view(self):
- # pylint: disable=protected-access
+ def test_workbench_scenarios(self):
"""
- Checks the student view for student specific instance variables.
+ Checks workbench scenarios title and basic scenario
"""
- student_view_html = self.student_view_html()
- self.assertIn(self.xblock.display_name, student_view_html)
- self.assertIn(self.xblock.prompt, student_view_html)
- self.assertIn(self.xblock._get_indicator_class(), student_view_html)
- self.assertIn(self.xblock._get_problem_progress(), student_view_html)
+ result_title = 'Free-text Response XBlock'
+ basic_scenario = "
"
+ test_result = self.xblock.workbench_scenarios()
+ self.assertEquals(result_title, test_result[0][0])
+ self.assertIn(basic_scenario, test_result[0][1])
- def test_studio_view(self):
+ def test_generate_validation_message(self):
+ # pylint: disable=invalid-name, protected-access
"""
- Checks studio view for instance variables specified by the instructor.
+ Checks classmethod _generate_validation_message
"""
- studio_view_html = self.studio_view_html()
- self.assertIn(self.xblock.display_name, studio_view_html)
- self.assertIn(self.xblock.prompt, studio_view_html)
- self.assertIn(str(self.xblock.max_attempts), studio_view_html)
- self.assertIn(str(self.xblock.display_correctness), studio_view_html)
- self.assertIn(str(self.xblock.min_word_count), studio_view_html)
- self.assertIn(str(self.xblock.max_word_count), studio_view_html)
- self.assertIn(
- ', '.join(
- self.xblock.fullcredit_keyphrases,
- ),
- studio_view_html,
+ msg = u'weight attempts cannot be negative'
+ result = ValidationMessage(
+ ValidationMessage.ERROR,
+ _(msg)
)
- self.assertIn(
- ', '.join(
- self.xblock.halfcredit_keyphrases,
- ),
- studio_view_html,
+ test_result = FreeTextResponse._generate_validation_message(msg)
+ self.assertEquals(
+ type(result),
+ type(test_result),
+ )
+ self.assertEquals(
+ result.text,
+ test_result.text,
+ )
+
+ @ddt.file_data('./tests/validate_field_data.json')
+ def test_validate_field_data(self, **test_dict):
+ """
+ Checks classmethod validate_field_data
+ tests the instuctor values set in edit
+ """
+ test_data = TestData()
+ test_data.weight = test_dict['weight']
+ test_data.max_attempts = test_dict['max_attempts']
+ test_data.max_word_count = test_dict['max_word_count']
+ test_data.min_word_count = test_dict['min_word_count']
+ test_data.submitted_message = test_dict['submitted_message']
+ validation = set()
+ self.xblock.validate_field_data(validation, test_data)
+ validation_list = list(validation)
+ # Only one validation error should be in set
+ self.assertEquals(1, len(validation_list))
+ self.assertEquals(
+ test_dict['result'],
+ validation_list[0].text,
+ )
+
+ def test_get_resource_string(self):
+ # pylint: disable=protected-access
+ """
+ Checks that get_resource_string returns the proper html
+ """
+ student_view_html = self.xblock.student_view().content
+ test_result = FreeTextResponse.get_resource_string('view.html')
+ test_result = test_result.format(
+ self=self.xblock,
+ word_count_message=self.xblock._get_word_count_message(),
+ indicator_class=self.xblock._get_indicator_class(),
+ problem_progress=self.xblock._get_problem_progress(),
+ used_attempts_feedback=self.xblock._get_used_attempts_feedback(),
+ nodisplay_class=self.xblock._get_nodisplay_class(),
+ visibility_class=self.xblock._get_indicator_visibility_class(),
+ submitted_message='',
+ user_alert='',
+ )
+ self.assertEquals(student_view_html, test_result)
+
+ @ddt.data('view.js.min.js', 'view.less.min.css')
+ def test_get_resource_url(self, path):
+ """
+ Checks that get_resource_url the correct url
+ """
+ public_path = '/resource/freetextresponse/public'
+ result = "{public_path}/{path}".format(
+ public_path=public_path,
+ path=path,
+ )
+ self.xblock.runtime.local_resource_url = MagicMock(
+ return_value=result
+ )
+ test_result = self.xblock.get_resource_url(path)
+ self.assertEquals(result, test_result)
+
+ def test_build_fragment(self):
+ # pylint: disable=protected-access
+ """
+ Checks build_fragment content and added resources
+ """
+ # Get content
+ student_view = self.xblock.student_view()
+ student_view_html = student_view.content
+ # Build fragment
+ view_html = FreeTextResponse.get_resource_string('view.html')
+ view_html = view_html.format(
+ self=self.xblock,
+ word_count_message=self.xblock._get_word_count_message(),
+ indicator_class=self.xblock._get_indicator_class(),
+ problem_progress=self.xblock._get_problem_progress(),
+ used_attempts_feedback=self.xblock._get_used_attempts_feedback(),
+ nodisplay_class=self.xblock._get_nodisplay_class(),
+ visibility_class=self.xblock._get_indicator_visibility_class(),
+ submitted_message='',
+ user_alert='',
+ )
+ public_path = '/resource/freetextresponse/public'
+ css_path = "{public_path}/{path}".format(
+ public_path=public_path,
+ path='view.less.min.css',
+ )
+ path_js = "{public_path}/{path}".format(
+ public_path=public_path,
+ path='view.js.min.js',
)
+ css_url = "{public_path}/{path}".format(
+ public_path=public_path,
+ path='some.fake.css',
+ )
+ js_url = "{public_path}/{path}".format(
+ public_path=public_path,
+ path='some.fake.js',
+ )
+ self.xblock.runtime.local_resource_url = MagicMock()
+ # Called once for each path
+ self.xblock.runtime.local_resource_url.side_effect = [
+ css_path,
+ path_js
+ ]
+ test_result = self.xblock.build_fragment(
+ html_source=view_html,
+ paths_css=[css_path],
+ paths_js=[path_js],
+ urls_css=[css_url],
+ urls_js=[js_url],
+ fragment_js='FreeTextResponseView',
+ )
+ self.assertEqual(student_view_html, test_result.content)
+ test_result_resource_names = []
+ for resource in test_result.resources:
+ test_result_resource_names.append(resource.data)
+ self.assertIn(css_path, test_result_resource_names)
+ self.assertIn(path_js, test_result_resource_names)
+ self.assertIn(css_url, test_result_resource_names)
+ self.assertIn(js_url, test_result_resource_names)
def test_initialization_variables(self):
"""
- Checks that all instance variables are initialized correctly
+ Checks that instance variables are initialized correctly
"""
self.assertEquals('Free-text Response', self.xblock.display_name)
self.assertEquals(
@@ -112,467 +240,412 @@ def test_initialization_variables(self):
self.assertEquals('', self.xblock.student_answer)
self.assertEquals(0, self.xblock.count_attempts)
- def student_view_html(self):
- """
- Helper method that returns the html of student_view
- """
- return self.xblock.student_view().content
-
- def studio_view_html(self):
+ # Default Views
+ def test_student_view(self):
+ # pylint: disable=protected-access
"""
- Helper method that returns the html of studio_view
+ Checks the student view for student specific instance variables.
"""
- return self.xblock.studio_view(context=None).content
+ student_view = self.xblock.student_view()
+ student_view_html = student_view.content
+ self.assertIn(self.xblock.display_name, student_view_html)
+ self.assertIn(self.xblock.prompt, student_view_html)
- def test_word_count_message_blank_when_attempts_0(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the word count message is blank when the
- user has made zero attempts
- """
- self.xblock.count_attempts = 0
- self.assertEquals(
- '',
- self.xblock._get_word_count_message(),
+ self.assertIn(self.xblock._get_word_count_message(), student_view_html)
+ self.assertIn(self.xblock._get_indicator_class(), student_view_html)
+ self.assertIn(self.xblock._get_problem_progress(), student_view_html)
+ self.assertIn(
+ self.xblock._get_used_attempts_feedback(),
+ student_view_html
)
-
- def test_word_count_message_not_blank_when_attempts_0(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the word count message is blank when the
- user has made zero attempts
- """
- self.xblock.count_attempts = 0
+ self.assertIn(self.xblock._get_nodisplay_class(), student_view_html)
self.assertIn(
- _('Invalid Word Count. Your response must be between'),
- self.xblock._get_word_count_message(
- ignore_attempts=True,
- ),
+ self.xblock._get_indicator_visibility_class(),
+ student_view_html
)
- def test_word_count_message_blank_when_word_count_valid(self):
- # pylint: disable=invalid-name, protected-access
+ def test_studio_view(self):
"""
- Tests that the word count message doesn't display when
- the word count is valid
+ Checks studio view for instance variables specified by the instructor.
"""
- self.xblock.count_attempts = 5
- self.xblock._word_count_valid = MagicMock(return_value=True)
- self.assertEquals(
- '',
- self.xblock._get_word_count_message()
+ studio_view_html = self.xblock.studio_view(context=None).content
+ self.assertIn(self.xblock.display_name, studio_view_html)
+ self.assertIn(self.xblock.prompt, studio_view_html)
+ self.assertIn(str(self.xblock.weight), studio_view_html)
+ self.assertIn(str(self.xblock.max_attempts), studio_view_html)
+ self.assertIn(str(self.xblock.display_correctness), studio_view_html)
+ self.assertIn(str(self.xblock.min_word_count), studio_view_html)
+ self.assertIn(str(self.xblock.max_word_count), studio_view_html)
+ self.assertIn(
+ ', '.join(
+ self.xblock.fullcredit_keyphrases,
+ ),
+ studio_view_html,
)
-
- def test_invalid_word_count_message(self):
- # pylint: disable=protected-access
- """
- Tests that the invalid word count message displays
- when appropriate
- """
- count_attempts = 5
- self.xblock._word_count_valid = MagicMock(return_value=False)
self.assertIn(
- _('Invalid Word Count. Your response must be between'),
- self.xblock._get_word_count_message(count_attempts)
+ ', '.join(
+ self.xblock.halfcredit_keyphrases,
+ ),
+ studio_view_html,
)
+ self.assertIn(str(self.xblock.submitted_message), studio_view_html)
- def test_indicator_class_unanswered(self):
- # pylint: disable=protected-access
- """
- Tests that the 'unanswered' class for the display_correctness
- html component displays when appropriate
- """
- self.xblock.student_answer = ''
- self.xblock.count_attempts = 0
- self.assertEquals('unanswered', self.xblock._get_indicator_class())
-
- def test_indicator_class_incorrect_blank_response(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the 'incorrect' class for the display_correctness html
- component displays when the response is blank
- """
- self.xblock.student_answer = ''
- self.xblock.count_attempts = 5
- self.assertEquals('incorrect', self.xblock._get_indicator_class())
-
- def test_indicator_class_incorrect_normal_response(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the 'incorrect' class for the display_correctness
- html component displays when the response is incorrect
- """
- self.xblock.student_answer = 'Non-blank response'
- self.xblock.count_attempts = 5
- self.xblock._word_count_valid = MagicMock(return_value=False)
- original = FreeTextResponse._is_at_least_one_phrase_present
- FreeTextResponse._is_at_least_one_phrase_present = \
- MagicMock(return_value=False)
- self.assertEquals('incorrect', self.xblock._get_indicator_class())
- FreeTextResponse._is_at_least_one_phrase_present = original
-
- def test_indicator_class_correct_normal_response(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the 'correct' class for the display_correctness html
- component displays when the response is correct
- """
- self.xblock.student_answer = 'Non-blank response'
- self.xblock.count_attempts = 5
- self.xblock._word_count_valid = MagicMock(return_value=True)
- original = FreeTextResponse._is_at_least_one_phrase_present
- FreeTextResponse._is_at_least_one_phrase_present = \
- MagicMock(return_value=True)
- self.assertEquals('correct', self.xblock._get_indicator_class())
- FreeTextResponse._is_at_least_one_phrase_present = original
-
- def test_word_count_in_range(self):
- # pylint: disable=protected-access
- """
- Tests that the word_count_valid method returns the
- appropriate response when the word count is valid
- """
- self.xblock.student_answer = 'One two three'
- self.xblock.min_word_count = 1
- self.xblock.max_word_count = 5
- self.assertTrue(self.xblock._word_count_valid())
-
- def test_word_count_min(self):
- # pylint: disable=protected-access
- """
- Tests that the word_count_valid method returns the
- appropriate response when the student's answer has
- the minimum number of permissible words
- """
- self.xblock.student_answer = 'One two three'
- self.xblock.min_word_count = 3
- self.xblock.max_word_count = 5
- self.assertTrue(self.xblock._word_count_valid())
-
- def test_word_count_max(self):
- # pylint: disable=protected-access
- """
- Tests that the word_count_valid method returns the
- appropriate response when the student's answer has
- the maximum number of permissible words
- """
- self.xblock.student_answer = 'One two three'
- self.xblock.min_word_count = 0
- self.xblock.max_word_count = 3
- self.assertTrue(self.xblock._word_count_valid())
-
- def test_word_count_too_short(self):
+ # Scoring
+ @ddt.file_data('./tests/determine_credit.json')
+ def test_determine_credit(self, **test_data):
# pylint: disable=protected-access
"""
- Tests that the word_count_valid method returns the
- appropriate response when the student's answer
- is too short
+ Tests determine_credit
+ After a student response this function will
+ return the Credit enum full, half, or zero
"""
- self.xblock.student_answer = 'One two three'
- self.xblock.min_word_count = 4
- self.xblock.max_word_count = 5
- self.assertFalse(self.xblock._word_count_valid())
-
- def test_word_count_too_long(self):
+ self.xblock._word_count_valid = MagicMock(
+ return_value=test_data['word_count_valid']
+ )
+ self.xblock.fullcredit_keyphrases = test_data['fullcredit']
+ self.xblock.halfcredit_keyphrases = test_data['halfcredit']
+ self.xblock.student_answer = test_data['student_answer']
+ credit = Credit[test_data['credit']]
+ self.assertEquals(credit, self.xblock._determine_credit())
+
+ @ddt.data(Credit.zero, Credit.half, Credit.full)
+ def test_compute_score(self, credit):
# pylint: disable=protected-access
"""
- Tests that the word_count_valid method returns the
- appropriate response when the student's answer
- is too long
+ Tests _compute_score
+ After a student response this function will
+ set the xblock score and publish the grade
"""
- self.xblock.student_answer = 'One two three'
- self.xblock.min_word_count = 0
- self.xblock.max_word_count = 2
- self.assertFalse(self.xblock._word_count_valid())
+ self.xblock.runtime.publish = MagicMock(return_value=None)
+ self.xblock._determine_credit = MagicMock(return_value=credit)
+ self.xblock._compute_score()
+ self.xblock.runtime.publish.assert_called_with(
+ self.xblock,
+ 'grade',
+ {'value': credit.value, 'max_value': Credit.full.value},
+ )
- def test_phrase_present_in_answer(self):
+ def test_is_at_least_one_phrase_present(self):
# pylint: disable=invalid-name, protected-access
"""
- Tests the phrase_present_in_answer helper method, when at least
- of of the phrases is present in the answer
- """
- phrases = ["Battle Ends", "And", "Down", "Goes", "Charles' Father"]
- answer = "chArles' fAther"
+ Tests _is_at_least_one_phrase_present
+ Helper method to match student response
+ with given phrase list
+ """
+ keyphrases_list = [
+ 'do dict phrase',
+ 're dict phrase',
+ 'mi dict phrase',
+ 'fa dict phrase'
+ ]
+ answer = keyphrases_list[1]
+ answer = 'ajhsdfhjaefhaf ' + answer + 'jkfbaufebn; fuqv'
self.assertTrue(
FreeTextResponse._is_at_least_one_phrase_present(
- phrases,
+ keyphrases_list,
answer,
),
)
- def test_pattern_not_present_in_answer(self):
+ def test_not_is_at_least_one_phrase_present(self):
# pylint: disable=invalid-name, protected-access
"""
- Tests the phrase_present_in_answer helper method, when none of the
- phrases are present in the anaswer
- """
- phrases = ["Battle Ends", "And", "Down", "Goes", "Charles' Father"]
- answer = "cHarleS' mother went to the store to buy honey"
+ Tests _is_at_least_one_phrase_present
+ Helper method to match student response
+ with given phrase list
+ """
+ keyphrases_list = [
+ 'do dict phrase',
+ 're dict phrase',
+ 'mi dict phrase',
+ 'fa dict phrase'
+ ]
+ answer = 'so dict phrase'
+ answer = 'ajhsdfhjaefhaf ' + answer + 'jkfbaufebn; fuqv'
self.assertFalse(
FreeTextResponse._is_at_least_one_phrase_present(
- phrases,
+ keyphrases_list,
answer,
),
)
- def test_problem_progress_weight_zero(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the the string returned by get_problem_progress
- is blank when the weight of the problem is zero
+ @ddt.file_data('./tests/word_count_valid.json')
+ def test_word_count_valid(self, **test_data):
+ # pylint: disable=protected-access
"""
- self.xblock.score = 1
- self.xblock.weight = 0
- self.assertEquals('', self.xblock._get_problem_progress())
-
- def test_problem_progress_score_zero_weight_singular(self):
+ Tests _word_count_valid
+ After a student response this will
+ determine if the response meets word
+ count criteria
+ """
+ self.xblock.min_word_count = test_data['min_word_count']
+ self.xblock.max_word_count = test_data['max_word_count']
+ self.xblock.student_answer = test_data['student_answer']
+ self.assertEquals(test_data['result'], self.xblock._word_count_valid())
+
+ # Messages
+ @ddt.data(
+ # max_attempts, count_attempts, result
+ (0, 4, ''),
+ (1, 0, 'You have used 0 of 1 submission'),
+ (3, 2, 'You have used 2 of 3 submissions'),
+ )
+ @ddt.unpack
+ def test_used_attempts_feedback_normal(
+ self,
+ max_attempts,
+ count_attempts,
+ result
+ ):
# pylint: disable=invalid-name, protected-access
"""
- Tests that the the string returned by get_problem_progress
- when the weight of the problem is singular, and the score is zero
+ Tests get_used_attempts_feedback
+ Returns the used attempts feedback message
+ after a student response
"""
- self.xblock.score = 0
- self.xblock.weight = 1
+ self.xblock.max_attempts = max_attempts
+ self.xblock.count_attempts = count_attempts
self.assertEquals(
- _('(1 point possible)'),
- self.xblock._get_problem_progress(),
+ _(result),
+ self.xblock._get_used_attempts_feedback(),
)
- def test_problem_progress_score_zero_weight_plural(self):
- # pylint: disable=invalid-name, protected-access
+ @ddt.data(
+ # min_word_count, max_word_count, result
+ (0, 1, 'Your response must be between 0 and 1 word.'),
+ (2, 3, 'Your response must be between 2 and 3 words.'),
+ )
+ @ddt.unpack
+ def test_get_word_count_message(
+ self,
+ min_word_count,
+ max_word_count,
+ result,
+ ):
+ # pylint: disable=protected-access
"""
- Tests that the the string returned by get_problem_progress
- when the weight of the problem is plural, and the score is zero
+ Tests _get_word_count_message
+ Returns the word count message
+ based on instructor set word count
+ min and max
"""
- self.xblock.score = 0
- self.xblock.weight = 3
+ self.xblock.min_word_count = min_word_count
+ self.xblock.max_word_count = max_word_count
self.assertEquals(
- _('(3 points possible)'),
- self.xblock._get_problem_progress(),
+ _(result),
+ self.xblock._get_word_count_message(),
)
- def test_problem_progress_score_positive_weight_singular(self):
- # pylint: disable=invalid-name, protected-access
+ # Tested from get_user_alert
+ @ddt.file_data('./tests/invalid_word_count_message.json')
+ def test_get_user_alert(self, **test_data):
+ # pylint: disable=protected-access
"""
- Tests that the the string returned by get_problem_progress
- when the weight of the problem is singular, and the score is positive
+ Tests _get_user_alert
+ if the word count is invalid this will
+ return the invalid word count message
"""
- self.xblock.score = 1
- self.xblock.weight = 1
- self.assertEquals(
- _('(1/1 point)'),
- self.xblock._get_problem_progress(),
+ self.xblock._word_count_valid = MagicMock(
+ return_value=test_data['word_count_valid']
)
-
- def test_problem_progress_score_positive_weight_plural(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the the string returned by get_problem_progress
- when the weight of the problem is plural, and the score is positive
- """
- self.xblock.score = 1
- self.xblock.weight = 3
+ self.xblock.count_attempts = test_data['count_attempts']
+ self.xblock.min_word_count = test_data['min_word_count']
+ self.xblock.max_word_count = test_data['max_word_count']
self.assertEquals(
- _('(3/3 points)'),
- self.xblock._get_problem_progress(),
+ _(str(test_data['result'])),
+ self.xblock._get_user_alert(
+ ignore_attempts=test_data['ignore_attempts']
+ ),
)
- def test_compute_score_full_credit(self):
+ @ddt.data(
+ # word_count_valid, result
+ (False, ''),
+ (True, 'test submission received message'),
+ )
+ @ddt.unpack
+ def test_get_submitted_message(
+ self,
+ word_count_valid,
+ result
+ ):
# pylint: disable=protected-access
"""
- Tests that a full-credit grade is assigned when appropriate
+ Tests _get_submitted_message
+ Returns a message to display to
+ the user after they submit a
+ resopnse
"""
- def get_full_credit():
- """
- Side-effect that returns full credit
- """
- return Credit.full
- self.xblock.runtime.publish = MagicMock(return_value=None)
- self.xblock._determine_credit = MagicMock(side_effect=get_full_credit)
- self.xblock._compute_score()
- self.xblock.runtime.publish.assert_called_with(
- self.xblock,
- 'grade',
- {'value': Credit.full.value, 'max_value': Credit.full.value},
+ self.xblock._word_count_valid = MagicMock(
+ return_value=word_count_valid
+ )
+ self.xblock.submitted_message = 'test submission received message'
+ self.assertEquals(
+ _(result),
+ self.xblock._get_submitted_message(),
)
- def test_compute_score_half_credit(self):
+ @ddt.file_data('./tests/problem_progress.json')
+ def test_get_problem_progress(self, **test_data):
# pylint: disable=protected-access
"""
- Tests that a half-credit grade is assigned when appropriate
+ Tests _get_problem_progress
+ Score can be 0, 0.5, or 1
+ Return a message for current
+ problem progress
"""
- def get_half_credit():
- """
- Side-effect that returns half credit
- """
- return Credit.half
- self.xblock.runtime.publish = MagicMock(return_value=None)
- self.xblock._determine_credit = MagicMock(side_effect=get_half_credit)
- self.xblock._compute_score()
- self.xblock.runtime.publish.assert_called_with(
- self.xblock,
- 'grade',
- {'value': Credit.half.value, 'max_value': Credit.full.value},
+ self.xblock.weight = test_data['weight']
+ self.xblock.score = test_data['score']
+ self.assertEquals(
+ _(test_data['result']),
+ self.xblock._get_problem_progress(),
)
- def test_compute_score_no_credit(self):
+ # CSS Classes
+ @ddt.file_data('./tests/indicator_class.json')
+ def test_get_indicator_class(self, **test_data):
# pylint: disable=protected-access
"""
- Tests that a no-credit grade is assigned when appropriate
- """
- def get_no_credit():
- """
- Side-effect that returns no credit
- """
- return Credit.zero
- self.xblock.runtime.publish = MagicMock(return_value=None)
- self.xblock._determine_credit = MagicMock(side_effect=get_no_credit)
- self.xblock._compute_score()
- self.xblock.runtime.publish.assert_called_with(
- self.xblock,
- 'grade',
- {'value': Credit.zero.value, 'max_value': Credit.full.value},
+ Test _get_indicator_class
+ Returns the correctness CCS class
+ to show correct/incorrect/unanswered
+ UI
+ """
+ credit = None
+ if test_data['credit']:
+ credit = Credit[test_data['credit']]
+ self.xblock.display_correctness = test_data['display_correctness']
+ self.xblock._word_count_valid = MagicMock(
+ return_value=test_data['word_count_valid']
)
-
- def test_indicator_visibility_class_blank(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that the get_indicator_visibility_class helper
- returns a blank class when appropriate
- """
- self.xblock.display_correctness = True
+ self.xblock._determine_credit = MagicMock(return_value=credit)
self.assertEquals(
- '',
- self.xblock._get_indicator_visiblity_class(),
+ test_data['result'],
+ self.xblock._get_indicator_class()
)
- def test_indicator_visibility_class_hidden(self):
+ @ddt.data(
+ # display_correctness, result
+ (True, ''),
+ (False, 'hidden'),
+ )
+ @ddt.unpack
+ def test_get_indicator_visibility_class(self, display_correctness, result):
# pylint: disable=invalid-name, protected-access
"""
- Tests that the get_indicator_visibility_class helper
- returns 'hidden' class when appropriate
+ Tests _get_indicator_visibility_class
+ Return hidden or blank CCS class to
+ hide correctness UI
"""
- self.xblock.display_correctness = False
+ self.xblock.display_correctness = display_correctness
self.assertEquals(
- 'hidden',
- self.xblock._get_indicator_visiblity_class(),
+ result,
+ self.xblock._get_indicator_visibility_class(),
)
- def test_determine_zero_credit_blank_answer(self):
- # pylint: disable=invalid-name, protected-access
- """
- Placeholder Docstring
- """
- self.xblock.student_answer = ''
- self.xblock._word_count_valid = MagicMock(return_value=False)
- self.assertEquals(Credit.zero, self.xblock._determine_credit())
-
- def test_determine_zero_credit_normal_answer(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that determine_credit() returns zero-credit when appropriate
- """
- self.xblock.student_answer = 'Non-blank answer'
- self.xblock.fullcredit_keyphrases = ['Something else']
- self.xblock.halfcredit_keyphrases = ['Something else']
- self.xblock._word_count_valid = MagicMock(return_value=True)
- self.assertEquals(Credit.zero, self.xblock._determine_credit())
-
- def test_determine_half_credit(self):
- # pylint: disable=protected-access
- """
- Tests that determine_credit() returns half-credit when appropriate
- """
- self.xblock.student_answer = 'Non-blank answer'
- self.xblock._word_count_valid = MagicMock(return_value=True)
- self.xblock.fullcredit_keyphrases = ['Something else']
- self.xblock.halfcredit_keyphrases = ['Non-blank', 'answer']
- self.assertEquals(Credit.half, self.xblock._determine_credit())
-
- def test_determine_full_credit(self):
+ @ddt.file_data('./tests/submitdisplay_class.json')
+ def test_get_submitdisplay_class(self, **test_data):
# pylint: disable=protected-access
"""
- Tests that determine_credit() returns full-credit when appropriate
- """
- self.xblock.student_answer = 'Non-blank answer'
- self.xblock._word_count_valid = MagicMock(return_value=True)
- self.xblock.fullcredit_keyphrases = 'Non-blank, answer'
- self.xblock.halfcredit_keyphrases = 'Something else'
- self.assertEquals(Credit.full, self.xblock._determine_credit())
-
- def test_used_attempts_feedback_blank(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that get_used_attempts_feedback returns no feedback when
- appropriate
- """
- self.xblock.max_attempts = 0
- self.assertEquals('', self.xblock._get_used_attempts_feedback())
-
- def test_used_attempts_feedback_normal(self):
- # pylint: disable=invalid-name, protected-access
- """
- Tests that get_used_attempts_feedback returns the expected feedback
+ Tests _get_submitdisplay_class
+ Return blank or nodisplay CCS class
+ that hide the submit buttons after
+ a user has reached max_attempts
"""
- self.xblock.max_attempts = 5
- self.xblock.count_attempts = 3
+ self.xblock.max_attempts = test_data['max_attempts']
+ self.xblock.count_attempts = test_data['count_attempts']
self.assertEquals(
- _('You have used 3 of 5 submissions'),
- self.xblock._get_used_attempts_feedback(),
+ test_data['result'],
+ self.xblock._get_nodisplay_class()
)
- def test_submit_class_blank(self):
- # pylint: disable=protected-access
- """
- Tests that get_submit_class returns a blank value when appropriate
- """
- self.xblock.max_attempts = 0
- self.assertEquals('', self.xblock._get_submit_class())
-
- def test_submit_class_nodisplay(self):
+ def test_submit(self):
# pylint: disable=protected-access
"""
- Tests that get_submit_class returns the appropriate class
- when the number of attempts has exceeded the maximum number of
- permissable attempts
- """
- self.xblock.max_attempts = 5
- self.xblock.count_attempts = 6
- self.assertEquals('nodisplay', self.xblock._get_submit_class())
-
- def test_sm_blank_zero_attempts(self):
- # pylint: disable=protected-access, invalid-name
- """
- Tests that _get_submitted_message returns an empty string
- when the user has made 0 attempts
- """
- self.xblock.count_attempts = 0
- self.xblock.submitted_message = self.test_submitted_message
- self.assertEquals('', self.xblock._get_submitted_message())
-
- def test_sm_blank_word_count_invalid(self):
- # pylint: disable=protected-access, invalid-name
- """
- Tests that _get_submitted_message returns an empty string
- when the word count is not valid, but the user has made
- at least one attempt
+ Tests save_reponse results
"""
- self.xblock.count_attempts = 2
- self.xblock.submitted_message = self.test_submitted_message
- self.xblock._word_count_valid = MagicMock(return_value=False)
- self.assertEquals('', self.xblock._get_submitted_message())
+ data = json.dumps({'student_answer': 'asdf'})
+ request = TestRequest()
+ request.method = 'POST'
+ request.body = data
+ response = self.xblock.submit(request)
+ # Added for response json_body
+ # pylint: disable=no-member
+ self.assertEquals(
+ response.json_body['status'],
+ 'success'
+ )
+ self.assertEquals(
+ response.json_body['problem_progress'],
+ self.xblock._get_problem_progress()
+ )
+ self.assertEquals(
+ response.json_body['indicator_class'],
+ self.xblock._get_indicator_class()
+ )
+ self.assertEquals(
+ response.json_body['used_attempts_feedback'],
+ self.xblock._get_used_attempts_feedback()
+ )
+ self.assertEquals(
+ response.json_body['nodisplay_class'],
+ self.xblock._get_nodisplay_class()
+ )
+ self.assertEquals(
+ response.json_body['submitted_message'],
+ self.xblock._get_submitted_message()
+ )
+ self.assertEquals(
+ response.json_body['user_alert'],
+ self.xblock._get_user_alert(
+ ignore_attempts=True,
+ )
+ )
+ self.assertEquals(
+ response.json_body['visibility_class'],
+ self.xblock._get_indicator_visibility_class()
+ )
- def test_srm_nonblank(self):
+ def test_save_reponse(self):
# pylint: disable=protected-access
"""
- Tests that _get_submitted_message returns the designated
- message when the word count is valid and the user has made
- at least one attempt
+ Tests save_reponse results
"""
- self.xblock.count_attempts = 2
- self.xblock.submitted_message = self.test_submitted_message
- self.xblock._word_count_valid = MagicMock(return_value=True)
+ data = json.dumps({'student_answer': 'asdf'})
+ request = TestRequest()
+ request.method = 'POST'
+ request.body = data
+ response = self.xblock.save_reponse(request)
+ # Added for response json_body
+ # pylint: disable=no-member
self.assertEquals(
- self.test_submitted_message,
- self.xblock._get_submitted_message(),
+ response.json_body['status'],
+ 'success'
+ )
+ self.assertEquals(
+ response.json_body['problem_progress'],
+ self.xblock._get_problem_progress()
+ )
+ self.assertIsNone(
+ response.json_body.get('indicator_class', None),
+ )
+ self.assertEquals(
+ response.json_body['used_attempts_feedback'],
+ self.xblock._get_used_attempts_feedback()
+ )
+ self.assertEquals(
+ response.json_body['nodisplay_class'],
+ self.xblock._get_nodisplay_class()
+ )
+ self.assertEquals(
+ response.json_body['submitted_message'],
+ ''
+ )
+ self.assertEquals(
+ response.json_body['user_alert'],
+ self.xblock.saved_message
+ )
+ self.assertEquals(
+ response.json_body['visibility_class'],
+ self.xblock._get_indicator_visibility_class()
)
diff --git a/freetextresponse/tests/determine_credit.json b/freetextresponse/tests/determine_credit.json
new file mode 100644
index 00000000..0a27d3b8
--- /dev/null
+++ b/freetextresponse/tests/determine_credit.json
@@ -0,0 +1,75 @@
+{
+ "zero_empty_answer": {
+ "word_count_valid": true,
+ "fullcredit": [
+ "full answer one",
+ "full answer two"
+ ],
+ "halfcredit": [
+ "half answer one",
+ "half answer two"
+ ],
+ "student_answer": "",
+ "credit": "zero"
+ },
+ "zero_invalid_word_count": {
+ "word_count_valid": false,
+ "fullcredit": [
+ "full answer one",
+ "full answer two"
+ ],
+ "halfcredit": [
+ "half answer one",
+ "half answer two"
+ ],
+ "student_answer": "full answer one",
+ "credit": "zero"
+ },
+ "zero_wrong_answer": {
+ "word_count_valid": true,
+ "fullcredit": [
+ "full answer one",
+ "full answer two"
+ ],
+ "halfcredit": [
+ "half answer one",
+ "half answer two"
+ ],
+ "student_answer": "double answer one",
+ "credit": "zero"
+ },
+ "full_no_phrases": {
+ "word_count_valid": true,
+ "fullcredit": [],
+ "halfcredit": [],
+ "student_answer": "any thing will do",
+ "credit": "full"
+ },
+ "full_right_answer": {
+ "word_count_valid": true,
+ "fullcredit": [
+ "full answer one",
+ "full answer two"
+ ],
+ "halfcredit": [
+ "half answer one",
+ "half answer two"
+ ],
+ "student_answer": "full answer two",
+ "credit": "full"
+ },
+ "half_right_answer": {
+ "word_count_valid": true,
+ "fullcredit": [
+ "full answer one",
+ "full answer two"
+ ],
+ "halfcredit": [
+ "half answer one",
+ "half answer two"
+ ],
+ "student_answer": "half answer two",
+ "credit": "half"
+ }
+}
+
diff --git a/freetextresponse/tests/indicator_class.json b/freetextresponse/tests/indicator_class.json
new file mode 100644
index 00000000..fa9e3595
--- /dev/null
+++ b/freetextresponse/tests/indicator_class.json
@@ -0,0 +1,32 @@
+{
+ "unanswered_word_invalid": {
+ "display_correctness": true,
+ "word_count_valid": false,
+ "credit": null,
+ "result": "unanswered"
+ },
+ "unanswered_word_valid": {
+ "display_correctness": true,
+ "word_count_valid": false,
+ "credit": null,
+ "result": "unanswered"
+ },
+ "incorrect_zero_credit": {
+ "display_correctness": true,
+ "word_count_valid": true,
+ "credit": "zero",
+ "result": "incorrect"
+ },
+ "correct_half_credit": {
+ "display_correctness": true,
+ "word_count_valid": true,
+ "credit": "half",
+ "result": "correct"
+ },
+ "correct_full_credit": {
+ "display_correctness": true,
+ "word_count_valid": true,
+ "credit": "full",
+ "result": "correct"
+ }
+}
diff --git a/freetextresponse/tests/invalid_word_count_message.json b/freetextresponse/tests/invalid_word_count_message.json
new file mode 100644
index 00000000..6ee9a016
--- /dev/null
+++ b/freetextresponse/tests/invalid_word_count_message.json
@@ -0,0 +1,43 @@
+{
+ "valid_zero_count_attempts": {
+ "word_count_valid": true,
+ "count_attempts": 0,
+ "min_word_count": 1,
+ "max_word_count": 10000,
+ "ignore_attempts": false,
+ "result": ""
+ },
+ "valid_one_count_attempts": {
+ "word_count_valid": true,
+ "count_attempts": 1,
+ "min_word_count": 1,
+ "max_word_count": 10000,
+ "ignore_attempts": false,
+ "result": ""
+ },
+ "valid_ignore_count_attempts": {
+ "word_count_valid": true,
+ "count_attempts": 1,
+ "min_word_count": 1,
+ "max_word_count": 10000,
+ "ignore_attempts": true,
+ "result": ""
+ },
+ "plural_max_word_count": {
+ "word_count_valid": false,
+ "count_attempts": 1,
+ "min_word_count": 1,
+ "max_word_count": 10000,
+ "ignore_attempts": false,
+ "result": "Invalid Word Count. Your response must be between 1 and 10000 words."
+ },
+ "single_max_word_count": {
+ "word_count_valid": false,
+ "count_attempts": 1,
+ "min_word_count": 1,
+ "max_word_count": 1,
+ "ignore_attempts": false,
+ "result": "Invalid Word Count. Your response must be between 1 and 1 word."
+ }
+}
+
diff --git a/freetextresponse/tests/problem_progress.json b/freetextresponse/tests/problem_progress.json
new file mode 100644
index 00000000..b6a2b7ee
--- /dev/null
+++ b/freetextresponse/tests/problem_progress.json
@@ -0,0 +1,52 @@
+{
+ "no_weight_zero_score": {
+ "weight": 0,
+ "score": 0,
+ "result": ""
+ },
+ "one_weight_zero_score": {
+ "weight": 1,
+ "score": 0,
+ "result": "(1 point possible)"
+ },
+ "one_weight_zero_score": {
+ "weight": 1,
+ "score": 1,
+ "result": "(1/1 point)"
+ },
+ "plural_weight_zero_score": {
+ "weight": 5,
+ "score": 0,
+ "result": "(5 points possible)"
+ },
+ "plural_weight_half_score": {
+ "weight": 5,
+ "score": 0.5,
+ "result": "(2.5/5 points)"
+ },
+ "plural_weight_full_score": {
+ "weight": 5,
+ "score": 1,
+ "result": "(5/5 points)"
+ },
+ "large_even_weight_zero_score": {
+ "weight": 975312468,
+ "score": 0,
+ "result": "(975312468 points possible)"
+ },
+ "large_even_weight_half_score": {
+ "weight": 975312468,
+ "score": 0.5,
+ "result": "(487656234/975312468 points)"
+ },
+ "large_even_weight_half_score": {
+ "weight": 975312468,
+ "score": 1,
+ "result": "(975312468/975312468 points)"
+ },
+ "large_odd_weight_half_score": {
+ "weight": 7919,
+ "score": 0.5,
+ "result": "(3959.5/7919 points)"
+ }
+}
diff --git a/freetextresponse/tests/submitdisplay_class.json b/freetextresponse/tests/submitdisplay_class.json
new file mode 100644
index 00000000..621d38e9
--- /dev/null
+++ b/freetextresponse/tests/submitdisplay_class.json
@@ -0,0 +1,22 @@
+{
+ "empty_zero_max": {
+ "max_attempts": 0,
+ "count_attempts": 1,
+ "result": ""
+ },
+ "empty_under_max": {
+ "max_attempts": 3,
+ "count_attempts": 2,
+ "result": ""
+ },
+ "nodisplay_at_max": {
+ "max_attempts": 3,
+ "count_attempts": 3,
+ "result": "nodisplay"
+ },
+ "nodisplay_over_max": {
+ "max_attempts": 3,
+ "count_attempts": 4,
+ "result": "nodisplay"
+ }
+}
diff --git a/freetextresponse/tests/test_travis.py b/freetextresponse/tests/test_travis.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/freetextresponse/tests/validate_field_data.json b/freetextresponse/tests/validate_field_data.json
new file mode 100644
index 00000000..3510c75c
--- /dev/null
+++ b/freetextresponse/tests/validate_field_data.json
@@ -0,0 +1,43 @@
+{
+ "weight_attemps_negative": {
+ "weight": -1,
+ "max_attempts": 1,
+ "max_word_count": 1,
+ "min_word_count": 1,
+ "submitted_message": "s",
+ "result": "Weight Attempts cannot be negative"
+ },
+ "max_attemps_negative": {
+ "weight": 0,
+ "max_attempts": -1,
+ "max_word_count": 1,
+ "min_word_count": 1,
+ "submitted_message": "s",
+ "result": "Maximum Attempts cannot be negative"
+ },
+ "min_word_count_less_than_one": {
+ "weight": 0,
+ "max_attempts": 1,
+ "max_word_count": 1,
+ "min_word_count": 0,
+ "submitted_message": "s",
+ "result": "Minimum Word Count cannot be less than 1"
+ },
+ "min_not_greater_than_max": {
+ "weight": 0,
+ "max_attempts": 1,
+ "max_word_count": 1,
+ "min_word_count": 2,
+ "submitted_message": "s",
+ "result": "Minimum Word Count cannot be greater than Max Word Count"
+ },
+ "submission_message_blank": {
+ "weight": 0,
+ "max_attempts": 1,
+ "max_word_count": 3,
+ "min_word_count": 2,
+ "submitted_message": "",
+ "result": "Submission Received Message cannot be blank"
+ }
+}
+
diff --git a/freetextresponse/tests/word_count_valid.json b/freetextresponse/tests/word_count_valid.json
new file mode 100644
index 00000000..3d376e63
--- /dev/null
+++ b/freetextresponse/tests/word_count_valid.json
@@ -0,0 +1,38 @@
+{
+ "blank_answer": {
+ "min_word_count": 1,
+ "max_word_count": 2,
+ "student_answer": "",
+ "result": false
+ },
+ "min_answer": {
+ "min_word_count": 1,
+ "max_word_count": 2,
+ "student_answer": "One",
+ "result": true
+ },
+ "max_answer": {
+ "min_word_count": 1,
+ "max_word_count": 2,
+ "student_answer": "One Two",
+ "result": true
+ },
+ "too_long_answer": {
+ "min_word_count": 2,
+ "max_word_count": 4,
+ "student_answer": "One Two Three Four Five",
+ "result": false
+ },
+ "too_short_answer": {
+ "min_word_count": 2,
+ "max_word_count": 4,
+ "student_answer": "One",
+ "result": false
+ },
+ "in_range_answer": {
+ "min_word_count": 3,
+ "max_word_count": 6,
+ "student_answer": "One Two Three Four Five",
+ "result": true
+ }
+}
diff --git a/requirements.txt b/requirements.txt
index 1013dbce..d1531567 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+ddt
django<1.9,>=1.8
git+https://github.com/edx/XBlock.git#egg=XBlock
git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0
diff --git a/tox.ini b/tox.ini
index d6e634d4..7dbf487e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,7 +29,9 @@ setenv =
NOSE_COVER_TESTS=1
NOSE_WITH_COVERAGE=1
commands =
- {envpython} manage.py test
+ # Added so next command covers declarations properly
+ {envbindir}/coverage run --source=freetextresponse manage.py test
+ {envpython} manage.py test --cover-xml
[testenv:coveralls]
deps =