From f08600832401f071db9f3fbbedb09e62a52d2cc9 Mon Sep 17 00:00:00 2001 From: caseylitton Date: Fri, 30 Sep 2016 13:49:49 -0700 Subject: [PATCH 1/5] Update node dependencies --- package.json | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..3e1284f6 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "xblock-free-text-response", + "title": "FreeTextResponse XBlock", + "description": "Enables instructors to create questions with free-text responses.", + "version": "0.1.2", + "homepage": "https://github.com/Stanford-Online/xblock-free-text-response", + "author": { + "name": "Azim Pradhan", + "email": "azim.pradhan@gmail.com" + }, + "license": "AGPLv3", + "repository": { + "type": "git", + "url": "https://github.com/Stanford-Online/xblock-free-text-response.git" + }, + "bugs": { + "url": "https://github.com/Stanford-Online/xblock-free-text-response/issues" + }, + "scripts": { + "test": "grunt --verbose" + }, + "devDependencies": { + "grunt": "^0.4.5", + "grunt-contrib-jshint": "^1.0.0", + "grunt-contrib-concat": "^0.5.0", + "grunt-contrib-uglify": "^0.6.0", + "grunt-contrib-less": "^0.11.4", + "grunt-contrib-csslint": "^0.3.1", + "grunt-contrib-cssmin": "^0.10.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-contrib-copy": "^0.6.0", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-htmlmin": "^0.3.0" + }, + "keywords": [ + "openedx", + "xblock" + ] +} From cb8f9446d73ed1ec590bd60ec1dcb2389185f92b Mon Sep 17 00:00:00 2001 From: caseylitton Date: Fri, 30 Sep 2016 13:51:04 -0700 Subject: [PATCH 2/5] Increase size of prompt text input --- freetextresponse/freetextresponse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freetextresponse/freetextresponse.py b/freetextresponse/freetextresponse.py index d034fc3d..b28ed4c4 100644 --- a/freetextresponse/freetextresponse.py +++ b/freetextresponse/freetextresponse.py @@ -118,6 +118,7 @@ def workbench_scenarios(): ), default=_('Please enter your response within this text area'), scope=Scope.settings, + multiline_editor=True, ) submitted_message = String( display_name=_('Submission Received Message'), From f1766eb0d5f0e9995b20b3f37eb328b6a7277332 Mon Sep 17 00:00:00 2001 From: caseylitton Date: Tue, 4 Oct 2016 14:52:57 -0700 Subject: [PATCH 3/5] Make UI resemble capa text reponse --- freetextresponse/freetextresponse.py | 131 +++++++++++++----- freetextresponse/private/view.html | 29 ++-- freetextresponse/private/view.js | 72 ++++++++-- freetextresponse/private/view.less | 44 +++++- freetextresponse/public/view.html | 2 +- freetextresponse/public/view.js | 72 ++++++++-- freetextresponse/public/view.js.min.js | 2 +- freetextresponse/public/view.js.min.js.map | 2 +- freetextresponse/public/view.less | 44 +++++- freetextresponse/public/view.less.min.css | 2 +- freetextresponse/public/view.less.min.css.map | 2 +- 11 files changed, 314 insertions(+), 88 deletions(-) diff --git a/freetextresponse/freetextresponse.py b/freetextresponse/freetextresponse.py index b28ed4c4..9e80419b 100644 --- a/freetextresponse/freetextresponse.py +++ b/freetextresponse/freetextresponse.py @@ -36,7 +36,36 @@ def workbench_scenarios(): ('Free-text Response XBlock', ''' - + + + + + + + '''), ] @@ -139,6 +168,18 @@ def workbench_scenarios(): values={'min': 1}, scope=Scope.settings, ) + saved_message = String( + display_name=_('Draft Received Message'), + help=_( + 'This is the message students will see upon ' + 'submitting a draft response' + ), + default=_( + 'Your answers have been saved but not graded. ' + 'Click "Submit" to grade them.' + ), + scope=Scope.settings, + ) count_attempts = Integer( default=0, @@ -179,12 +220,10 @@ def student_view(self, context=None): indicator_class=self._get_indicator_class(), problem_progress=self._get_problem_progress(), used_attempts_feedback=self._get_used_attempts_feedback(), - submit_class=self._get_submit_class(), - indicator_visibility_class=self._get_indicator_visiblity_class(), - word_count_message=self._get_word_count_message( - self.count_attempts - ), - submitted_message=self._get_submitted_message(), + nodisplay_class=self._get_nodisplay_class(), + visibility_class=self._get_indicator_visibility_class(), + submitted_message='', + user_alert='', ) fragment = self.build_fragment( html_source=view_html, @@ -290,7 +329,7 @@ def build_fragment( fragment.initialize_js(fragment_js) return fragment - def _get_indicator_visiblity_class(self): + def _get_indicator_visibility_class(self): """ Returns the visibility class for the correctness indicator html element """ @@ -325,13 +364,12 @@ def _get_indicator_class(self): """ Returns the class of the correctness indicator element """ - result = '' - if self.count_attempts == 0: - result = 'unanswered' - elif self._determine_credit() == Credit.zero: - result = 'incorrect' - else: - result = 'correct' + result = 'unanswered' + if self.display_correctness and self._word_count_valid(): + if self._determine_credit() == Credit.zero: + result = 'incorrect' + else: + result = 'correct' return result def _word_count_valid(self): @@ -452,7 +490,7 @@ def _get_used_attempts_feedback(self): ) return result - def _get_submit_class(self): + def _get_nodisplay_class(self): """ Returns the css class for the submit button """ @@ -466,40 +504,65 @@ def _get_submitted_message(self): Returns the message to display in the submission-received div """ result = '' - if self.count_attempts > 0 and self._word_count_valid(): + if self._word_count_valid(): result = self.submitted_message return result + def _get_user_alert(self, ignore_attempts=False): + """ + Returns the message to display in the user_alert(TBD) div + """ + result = '' + if not self._word_count_valid(): + result = self._get_word_count_message(ignore_attempts) + return result + @XBlock.json_handler def submit(self, data, suffix=''): # pylint: disable=unused-argument """ Processes the user's submission """ - if self.max_attempts > 0 and self.count_attempts >= self.max_attempts: - raise StandardError( - _( - 'User has already exceeded the ' - 'maximum number of allowed attempts' - ) - ) - self.student_answer = data['student_answer'] - if self._word_count_valid(): - if self.max_attempts == 0: - self.count_attempts = 1 - else: + # Fails if the UI submit/save buttons were shut + # down on the previous sumbisson + if self.max_attempts == 0 or self.count_attempts < self.max_attempts: + self.student_answer = data['student_answer'] + if self._word_count_valid(): self.count_attempts += 1 - self._compute_score() + self._compute_score() result = { 'status': 'success', 'problem_progress': self._get_problem_progress(), 'indicator_class': self._get_indicator_class(), 'used_attempts_feedback': self._get_used_attempts_feedback(), - 'submit_class': self._get_submit_class(), - 'word_count_message': self._get_word_count_message( - ignore_attempts=True - ), + 'nodisplay_class': self._get_nodisplay_class(), 'submitted_message': self._get_submitted_message(), + 'user_alert': self._get_user_alert( + ignore_attempts=True, + ), + 'visibility_class': self._get_indicator_visibility_class(), + } + return result + + @XBlock.json_handler + def save_reponse(self, data, suffix=''): + # pylint: disable=unused-argument + """ + Processes the user's save + """ + # Fails if the UI submit/save buttons were shut + # down on the previous sumbisson + if self.max_attempts == 0 or self.count_attempts < self.max_attempts: + self.student_answer = data['student_answer'] + result = { + 'status': 'success', + 'problem_progress': self._get_problem_progress(), + 'indicator_class': self._get_indicator_class(), + 'used_attempts_feedback': self._get_used_attempts_feedback(), + 'nodisplay_class': self._get_nodisplay_class(), + 'submitted_message': '', + 'user_alert': self.saved_message, + 'visibility_class': self._get_indicator_visibility_class(), } return result diff --git a/freetextresponse/private/view.html b/freetextresponse/private/view.html index c0784c92..8f43a3bd 100644 --- a/freetextresponse/private/view.html +++ b/freetextresponse/private/view.html @@ -1,24 +1,27 @@ -
+

{self.display_name}

{problem_progress}

{self.prompt}

-
- - +
+
+ + +
-
+
{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..5cb75ce7 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; @@ -66,11 +93,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..b7f8ec45 100644 --- a/freetextresponse/public/view.html +++ b/freetextresponse/public/view.html @@ -1 +1 @@ -

{self.display_name}

{problem_progress}

{self.prompt}

{submitted_message}
{word_count_message}
{used_attempts_feedback}
\ No newline at end of file +

{self.display_name}

{problem_progress}

{self.prompt}

{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..5cb75ce7 100644 --- a/freetextresponse/public/view.less +++ b/freetextresponse/public/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; @@ -66,11 +93,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..dc097eb7 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 .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..50f24562 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,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,iBAEE,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,yBA5CN,iBAmDE,SACE,gBApDJ,iBAmDE,QAGE,OACE,WAAA,CACA,qBAAA,CACA,gBAzDN,iBAmDE,QASE,QACE,WAAA,CACA,qBAAA,CACA,gBA/DN,iBAmDE,QAeE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBAvEN,iBAmDE,QAuBE,YACE,aA3EN,iBA+EE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cApFJ,iBAuFE,SACE,kBAxFJ,iBA2FE,iBACE,qBA5FJ,iBA+FE,aACE,eAAA,CACA,gBAAA,CACA,wBAAA,CACA,iBAAA,CACA,kBAAA,CACA,gBArGJ,iBAwGE,IAAG,OAAS","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 .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 .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 From 496e4dc398102782e81b4bcd9749148412ac7d53 Mon Sep 17 00:00:00 2001 From: caseylitton Date: Fri, 18 Nov 2016 14:24:56 -0800 Subject: [PATCH 4/5] Add static word count message --- freetextresponse/freetextresponse.py | 35 +++++++++++++------ freetextresponse/private/view.html | 1 + freetextresponse/private/view.less | 8 +++++ freetextresponse/public/view.html | 2 +- freetextresponse/public/view.less | 8 +++++ freetextresponse/public/view.less.min.css | 2 +- freetextresponse/public/view.less.min.css.map | 2 +- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/freetextresponse/freetextresponse.py b/freetextresponse/freetextresponse.py index 9e80419b..fcd632c9 100644 --- a/freetextresponse/freetextresponse.py +++ b/freetextresponse/freetextresponse.py @@ -217,6 +217,7 @@ def student_view(self, context=None): view_html = FreeTextResponse.get_resource_string('view.html') view_html = view_html.format( self=self, + word_count_message=self._get_word_count_message(), indicator_class=self._get_indicator_class(), problem_progress=self._get_problem_progress(), used_attempts_feedback=self._get_used_attempts_feedback(), @@ -341,22 +342,33 @@ def _get_indicator_visibility_class(self): def _get_word_count_message(self, ignore_attempts=False): """ - Returns the word count message based on the student's answer + Returns the word count message + """ + result = ungettext( + "Your response must be " + "between {min} and {max} word.", + "Your response must be " + "between {min} and {max} words.", + self.max_word_count, + ).format( + min=self.min_word_count, + max=self.max_word_count, + ) + return result + + def _get_invalid_word_count_message(self, ignore_attempts=False): + """ + Returns the invalid word count message """ result = '' if ( (ignore_attempts or self.count_attempts > 0) and (not self._word_count_valid()) ): - result = ungettext( - "Invalid Word Count. Your response must be " - "between {min} and {max} word.", - "Invalid Word Count. Your response must be " - "between {min} and {max} words.", - self.max_word_count, + word_count_message = self._get_word_count_message(ignore_attempts=ignore_attempts) + result = _("Invalid Word Count. {word_count_message}" ).format( - min=self.min_word_count, - max=self.max_word_count, + word_count_message=word_count_message, ) return result @@ -510,11 +522,12 @@ def _get_submitted_message(self): def _get_user_alert(self, ignore_attempts=False): """ - Returns the message to display in the user_alert(TBD) div + Returns the message to display in the user_alert div + depending on the student answer """ result = '' if not self._word_count_valid(): - result = self._get_word_count_message(ignore_attempts) + result = self._get_invalid_word_count_message(ignore_attempts) return result @XBlock.json_handler diff --git a/freetextresponse/private/view.html b/freetextresponse/private/view.html index 8f43a3bd..ad5dd8fb 100644 --- a/freetextresponse/private/view.html +++ b/freetextresponse/private/view.html @@ -2,6 +2,7 @@

{self.display_name}

{problem_progress}

{self.prompt}

+
{word_count_message}
diff --git a/freetextresponse/private/view.less b/freetextresponse/private/view.less index 5cb75ce7..7d9ff34f 100644 --- a/freetextresponse/private/view.less +++ b/freetextresponse/private/view.less @@ -85,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; } diff --git a/freetextresponse/public/view.html b/freetextresponse/public/view.html index b7f8ec45..dfcab702 100644 --- a/freetextresponse/public/view.html +++ b/freetextresponse/public/view.html @@ -1 +1 @@ -

{self.display_name}

{problem_progress}

{self.prompt}

{submitted_message}
{used_attempts_feedback}
{user_alert}
\ No newline at end of file +

{self.display_name}

{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.less b/freetextresponse/public/view.less index 5cb75ce7..7d9ff34f 100644 --- a/freetextresponse/public/view.less +++ b/freetextresponse/public/view.less @@ -85,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; } diff --git a/freetextresponse/public/view.less.min.css b/freetextresponse/public/view.less.min.css index dc097eb7..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 .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 .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 +.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 50f24562..c0d9a1f6 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,iBAEE,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,yBA5CN,iBAmDE,SACE,gBApDJ,iBAmDE,QAGE,OACE,WAAA,CACA,qBAAA,CACA,gBAzDN,iBAmDE,QASE,QACE,WAAA,CACA,qBAAA,CACA,gBA/DN,iBAmDE,QAeE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBAvEN,iBAmDE,QAuBE,YACE,aA3EN,iBA+EE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cApFJ,iBAuFE,SACE,kBAxFJ,iBA2FE,iBACE,qBA5FJ,iBA+FE,aACE,eAAA,CACA,gBAAA,CACA,wBAAA,CACA,iBAAA,CACA,kBAAA,CACA,gBArGJ,iBAwGE,IAAG,OAAS","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 .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 .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 +{"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,iBAEE,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,yBA5CN,iBAmDE,SACE,gBApDJ,iBAmDE,QAGE,OACE,WAAA,CACA,qBAAA,CACA,gBAzDN,iBAmDE,QASE,QACE,WAAA,CACA,qBAAA,CACA,gBA/DN,iBAmDE,QAeE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBAvEN,iBAmDE,QAuBE,YACE,aA3EN,iBA+EE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cApFJ,iBAuFE,qBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cA5FJ,iBA+FE,SACE,kBAhGJ,iBAmGE,iBACE,qBApGJ,iBAuGE,aACE,eAAA,CACA,gBAAA,CACA,wBAAA,CACA,iBAAA,CACA,kBAAA,CACA,gBA7GJ,iBAgHE,IAAG,OAAS","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 .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 From 94797a84eaa7c5be1e16acc7a2b4729699db79ff Mon Sep 17 00:00:00 2001 From: caseylitton Date: Fri, 18 Nov 2016 14:36:31 -0800 Subject: [PATCH 5/5] Update test after tox fix --- .gitignore | 1 + freetextresponse/freetextresponse.py | 31 +- freetextresponse/public/view.less | 1 + freetextresponse/public/view.less.min.css.map | 2 +- freetextresponse/tests.py | 913 ++++++++++-------- freetextresponse/tests/determine_credit.json | 75 ++ freetextresponse/tests/indicator_class.json | 32 + .../tests/invalid_word_count_message.json | 43 + freetextresponse/tests/problem_progress.json | 52 + .../tests/submitdisplay_class.json | 22 + freetextresponse/tests/test_travis.py | 0 .../tests/validate_field_data.json | 43 + freetextresponse/tests/word_count_valid.json | 38 + package.json | 39 - requirements.txt | 1 + tox.ini | 4 +- 16 files changed, 821 insertions(+), 476 deletions(-) create mode 100644 freetextresponse/tests/determine_credit.json create mode 100644 freetextresponse/tests/indicator_class.json create mode 100644 freetextresponse/tests/invalid_word_count_message.json create mode 100644 freetextresponse/tests/problem_progress.json create mode 100644 freetextresponse/tests/submitdisplay_class.json delete mode 100644 freetextresponse/tests/test_travis.py create mode 100644 freetextresponse/tests/validate_field_data.json create mode 100644 freetextresponse/tests/word_count_valid.json delete mode 100644 package.json diff --git a/.gitignore b/.gitignore index dc75bca0..3d5fe8e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ *.pyc *.sw[op] .coverage +coverage.xml .tox/ diff --git a/freetextresponse/freetextresponse.py b/freetextresponse/freetextresponse.py index fcd632c9..c0742e50 100644 --- a/freetextresponse/freetextresponse.py +++ b/freetextresponse/freetextresponse.py @@ -5,7 +5,6 @@ import os import pkg_resources -from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from enum import Enum from xblock.core import XBlock @@ -36,10 +35,14 @@ def workbench_scenarios(): ('Free-text Response XBlock', ''' + 0) and (not self._word_count_valid()) ): - word_count_message = self._get_word_count_message(ignore_attempts=ignore_attempts) - result = _("Invalid Word Count. {word_count_message}" + word_count_message = self._get_word_count_message() + result = _( + "Invalid Word Count. {word_count_message}" ).format( word_count_message=word_count_message, ) @@ -428,7 +428,8 @@ def _get_problem_progress(self): ) else: scaled_score = self.score * self.weight - score_string = '{0:g}'.format(scaled_score) + # No trailing zero and no scientific notation + score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.') result = "({})".format( ungettext( "{score_string}/{weight} point", @@ -540,9 +541,10 @@ def submit(self, data, suffix=''): # down on the previous sumbisson if self.max_attempts == 0 or self.count_attempts < self.max_attempts: self.student_answer = data['student_answer'] - if self._word_count_valid(): - self.count_attempts += 1 - self._compute_score() + # Counting the attempts and publishing a score + # even if word count is invalid. + self.count_attempts += 1 + self._compute_score() result = { 'status': 'success', 'problem_progress': self._get_problem_progress(), @@ -570,7 +572,6 @@ def save_reponse(self, data, suffix=''): result = { 'status': 'success', 'problem_progress': self._get_problem_progress(), - 'indicator_class': self._get_indicator_class(), 'used_attempts_feedback': self._get_used_attempts_feedback(), 'nodisplay_class': self._get_nodisplay_class(), 'submitted_message': '', diff --git a/freetextresponse/public/view.less b/freetextresponse/public/view.less index 7d9ff34f..bc9d8924 100644 --- a/freetextresponse/public/view.less +++ b/freetextresponse/public/view.less @@ -1,5 +1,6 @@ .freetextresponse { + .user_input { .status { diff --git a/freetextresponse/public/view.less.min.css.map b/freetextresponse/public/view.less.min.css.map index c0d9a1f6..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,iBAEE,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,yBA5CN,iBAmDE,SACE,gBApDJ,iBAmDE,QAGE,OACE,WAAA,CACA,qBAAA,CACA,gBAzDN,iBAmDE,QASE,QACE,WAAA,CACA,qBAAA,CACA,gBA/DN,iBAmDE,QAeE,yBACE,oBAAA,CACA,cAAA,CACA,gBAAA,CACA,UAAA,CACA,kBAvEN,iBAmDE,QAuBE,YACE,aA3EN,iBA+EE,mBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cApFJ,iBAuFE,qBACE,oBAAA,CACA,gBAAA,CACA,UAAA,CACA,eAAA,CACA,cA5FJ,iBA+FE,SACE,kBAhGJ,iBAmGE,iBACE,qBApGJ,iBAuGE,aACE,eAAA,CACA,gBAAA,CACA,wBAAA,CACA,iBAAA,CACA,kBAAA,CACA,gBA7GJ,iBAgHE,IAAG,OAAS","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 .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 +{"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/package.json b/package.json deleted file mode 100644 index 3e1284f6..00000000 --- a/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "xblock-free-text-response", - "title": "FreeTextResponse XBlock", - "description": "Enables instructors to create questions with free-text responses.", - "version": "0.1.2", - "homepage": "https://github.com/Stanford-Online/xblock-free-text-response", - "author": { - "name": "Azim Pradhan", - "email": "azim.pradhan@gmail.com" - }, - "license": "AGPLv3", - "repository": { - "type": "git", - "url": "https://github.com/Stanford-Online/xblock-free-text-response.git" - }, - "bugs": { - "url": "https://github.com/Stanford-Online/xblock-free-text-response/issues" - }, - "scripts": { - "test": "grunt --verbose" - }, - "devDependencies": { - "grunt": "^0.4.5", - "grunt-contrib-jshint": "^1.0.0", - "grunt-contrib-concat": "^0.5.0", - "grunt-contrib-uglify": "^0.6.0", - "grunt-contrib-less": "^0.11.4", - "grunt-contrib-csslint": "^0.3.1", - "grunt-contrib-cssmin": "^0.10.0", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-copy": "^0.6.0", - "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-htmlmin": "^0.3.0" - }, - "keywords": [ - "openedx", - "xblock" - ] -} 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 =