Skip to content
Snippets Groups Projects
Commit 28501ca5 authored by Diana Huang's avatar Diana Huang
Browse files

Merge branch 'master' into diana/peer-grading-views

parents 078b2a5b 559bf887
No related merge requests found
Showing
with 1026 additions and 21 deletions
......@@ -186,6 +186,24 @@ class LoncapaProblem(object):
maxscore += responder.get_max_score()
return maxscore
def message_post(self,event_info):
"""
Handle an ajax post that contains feedback on feedback
Returns a boolean success variable
Note: This only allows for feedback to be posted back to the grading controller for the first
open ended response problem on each page. Multiple problems will cause some sync issues.
TODO: Handle multiple problems on one page sync issues.
"""
success=False
message = "Could not find a valid responder."
log.debug("in lcp")
for responder in self.responders.values():
if hasattr(responder, 'handle_message_post'):
success, message = responder.handle_message_post(event_info)
if success:
break
return success, message
def get_score(self):
"""
Compute score for this problem. The score is the number of points awarded.
......
......@@ -748,7 +748,7 @@ class OpenEndedInput(InputTypeBase):
# pulled out for testing
submitted_msg = ("Feedback not yet available. Reload to check again. "
"Once the problem is graded, this message will be "
"replaced with the grader's feedback")
"replaced with the grader's feedback.")
@classmethod
def get_attributes(cls):
......
......@@ -1836,6 +1836,7 @@ class OpenEndedResponse(LoncapaResponse):
"""
DEFAULT_QUEUE = 'open-ended'
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
response_tag = 'openendedresponse'
allowed_inputfields = ['openendedinput']
max_inputfields = 1
......@@ -1847,12 +1848,17 @@ class OpenEndedResponse(LoncapaResponse):
xml = self.xml
self.url = xml.get('url', None)
self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE)
self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
# The openendedparam tag encapsulates all grader settings
oeparam = self.xml.find('openendedparam')
prompt = self.xml.find('prompt')
rubric = self.xml.find('openendedrubric')
#This is needed to attach feedback to specific responses later
self.submission_id=None
self.grader_id=None
if oeparam is None:
raise ValueError("No oeparam found in problem xml.")
if prompt is None:
......@@ -1899,23 +1905,81 @@ class OpenEndedResponse(LoncapaResponse):
# response types)
except TypeError, ValueError:
log.exception("Grader payload %r is not a json object!", grader_payload)
self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
parsed_grader_payload.update({
'location' : self.system.location,
'course_id' : self.system.course_id,
'prompt' : prompt_string,
'rubric' : rubric_string,
})
'initial_display' : self.initial_display,
'answer' : self.answer,
})
updated_grader_payload = json.dumps(parsed_grader_payload)
self.payload = {'grader_payload': updated_grader_payload}
self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
try:
self.max_score = int(find_with_default(oeparam, 'max_score', 1))
except ValueError:
self.max_score = 1
def handle_message_post(self,event_info):
"""
Handles a student message post (a reaction to the grade they received from an open ended grader type)
Returns a boolean success/fail and an error message
"""
survey_responses=event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
return False, "Could not find needed tag {0}".format(tag)
try:
submission_id=int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id'])
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
score = int(survey_responses['score'])
except:
error_message=("Could not parse submission id, grader id, "
"or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses))
log.exception(error_message)
return False, "There was an error saving your feedback. Please contact course staff."
qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
anonymous_student_id = self.system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
anonymous_student_id +
self.answer_id)
xheader = xqueue_interface.make_xheader(
lms_callback_url=self.system.xqueue['callback_url'],
lms_key=queuekey,
queue_name=self.message_queue_name
)
student_info = {'anonymous_student_id': anonymous_student_id,
'submission_time': qtime,
}
contents= {
'feedback' : feedback,
'submission_id' : submission_id,
'grader_id' : grader_id,
'score': score,
'student_info' : json.dumps(student_info),
}
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
#Convert error to a success value
success=True
if error:
success=False
return success, "Successfully submitted your feedback."
def get_score(self, student_answers):
try:
......@@ -1956,7 +2020,7 @@ class OpenEndedResponse(LoncapaResponse):
contents.update({
'student_info': json.dumps(student_info),
'student_response': submission,
'max_score' : self.max_score
'max_score' : self.max_score,
})
# Submit request. When successful, 'msg' is the prior length of the queue
......@@ -2056,18 +2120,36 @@ class OpenEndedResponse(LoncapaResponse):
"""
return priorities.get(elt[0], default_priority)
def encode_values(feedback_type,value):
feedback_type=str(feedback_type).encode('ascii', 'ignore')
if not isinstance(value,basestring):
value=str(value)
value=value.encode('ascii', 'ignore')
return feedback_type,value
def format_feedback(feedback_type, value):
return """
feedback_type,value=encode_values(feedback_type,value)
feedback= """
<div class="{feedback_type}">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
def format_feedback_hidden(feedback_type , value):
feedback_type,value=encode_values(feedback_type,value)
feedback = """
<div class="{feedback_type}" style="display: none;">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
# TODO (vshnayder): design and document the details of this format so
# that we can do proper escaping here (e.g. are the graders allowed to
# include HTML?)
for tag in ['success', 'feedback']:
for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
if tag not in response_items:
return format_feedback('errors', 'Error getting feedback')
......@@ -2083,10 +2165,15 @@ class OpenEndedResponse(LoncapaResponse):
return format_feedback('errors', 'No feedback available')
feedback_lst = sorted(feedback.items(), key=get_priority)
return u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
else:
return format_feedback('errors', response_items['feedback'])
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value)
for feedback_type,value in response_items.items()
if feedback_type in ['submission_id', 'grader_id']]))
return u"\n".join([feedback_list_part1,feedback_list_part2])
def _format_feedback(self, response_items):
"""
......@@ -2104,7 +2191,7 @@ class OpenEndedResponse(LoncapaResponse):
feedback_template = self.system.render_template("open_ended_feedback.html", {
'grader_type': response_items['grader_type'],
'score': response_items['score'],
'score': "{0} / {1}".format(response_items['score'], self.max_score),
'feedback': feedback,
})
......@@ -2138,17 +2225,19 @@ class OpenEndedResponse(LoncapaResponse):
" Received score_result = {0}".format(score_result))
return fail
for tag in ['score', 'feedback', 'grader_type', 'success']:
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
if tag not in score_result:
log.error("External grader message is missing required tag: {0}"
.format(tag))
return fail
feedback = self._format_feedback(score_result)
self.submission_id=score_result['submission_id']
self.grader_id=score_result['grader_id']
# HACK: for now, just assume it's correct if you got more than 2/3.
# Also assumes that score_result['score'] is an integer.
score_ratio = int(score_result['score']) / self.max_score
score_ratio = int(score_result['score']) / float(self.max_score)
correct = (score_ratio >= 0.66)
#Currently ignore msg and only return feedback (which takes the place of msg)
......
......@@ -27,6 +27,30 @@
<input name="reload" class="reload" type="button" value="Recheck for Feedback" onclick="document.location.reload(true);" />
% endif
<div class="external-grader-message">
${msg|n}
${msg|n}
% if status in ['correct','incorrect']:
<div class="collapsible evaluation-response">
<header>
<a href="#">Respond to Feedback</a>
</header>
<section id="evaluation_${id}" class="evaluation">
<p>How accurate do you find this feedback?</p>
<div class="evaluation-scoring">
<ul class="scoring-list">
<li><input type="radio" name="evaluation-score" id="evaluation-score-5" value="5" /> <label for="evaluation-score-5"> Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-4" value="4" /> <label for="evaluation-score-4"> Partially Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-3" value="3" /> <label for="evaluation-score-3"> No Opinion</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-2" value="2" /> <label for="evaluation-score-2"> Partially Incorrect</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-1" value="1" /> <label for="evaluation-score-1"> Incorrect</label></li>
</ul>
</div>
<p>Additional comments:</p>
<textarea rows="${rows}" cols="${cols}" name="feedback_${id}" class="feedback-on-feedback" id="feedback_${id}"></textarea>
<div class="submit-message-container">
<input name="submit-message" class="submit-message" type="button" value="Submit your message"/>
</div>
</section>
</div>
% endif
</div>
</section>
......@@ -380,6 +380,7 @@ class CapaModule(XModule):
'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
'message_post' : self.message_post,
}
if dispatch not in handlers:
......@@ -394,6 +395,20 @@ class CapaModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
def message_post(self, get):
"""
Posts a message from a form to an appropriate location
"""
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
event_info['student_id'] = self.system.anonymous_student_id
event_info['survey_responses']= get
success, message = self.lcp.message_post(event_info)
return {'success' : success, 'message' : message}
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
......
......@@ -297,6 +297,51 @@ section.problem {
float: left;
}
}
}
.evaluation {
p {
margin-bottom: 4px;
}
}
.feedback-on-feedback {
height: 100px;
margin-right: 20px;
}
.evaluation-response {
header {
text-align: right;
a {
font-size: .85em;
}
}
}
.evaluation-scoring {
.scoring-list {
list-style-type: none;
margin-left: 3px;
li {
&:first-child {
margin-left: 0px;
}
display:inline;
margin-left: 50px;
label {
font-size: .9em;
}
}
}
}
.submit-message-container {
margin: 10px 0px ;
}
}
......@@ -634,6 +679,10 @@ section.problem {
color: #2C2C2C;
font-family: monospace;
font-size: 1em;
padding-top: 10px;
header {
font-size: 1.4em;
}
.shortform {
font-weight: bold;
......
......@@ -25,6 +25,7 @@ class @Problem
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
@$('section.evaluation input.submit-message').click @message_post
# Collapsibles
Collapsible.setCollapsibles(@el)
......@@ -197,6 +198,35 @@ class @Problem
else
@gentle_alert response.success
message_post: =>
Logger.log 'message_post', @answers
fd = new FormData()
feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value
submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML
grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML
score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val()
fd.append('feedback', feedback)
fd.append('submission_id', submission_id)
fd.append('grader_id', grader_id)
if(!score)
@gentle_alert "You need to pick a rating before you can submit."
return
else
fd.append('score', score)
settings =
type: "POST"
data: fd
processData: false
contentType: false
success: (response) =>
@gentle_alert response.message
@$('section.evaluation').slideToggle()
$.ajaxWithPrefix("#{@url}/message_post", settings)
reset: =>
Logger.log 'problem_reset', @answers
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>
......
/*
* This file is a wrapper for the Require JS file and module loader. Please see
* the discussion at:
*
* https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
*/
var RequireJS = function() {
// Below is the unmodified minified version of Require JS. The latest can be
// found at:
//
// http://requirejs.org/docs/download.html
/*
RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
Available via the MIT or new BSD license.
see: http://github.com/jrburke/requirejs for details
*/
var requirejs,require,define;
(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function M(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function r(b,c){return da.call(b,c)}function i(b,c){return r(b,c)&&b[c]}function E(b,c){for(var d in b)if(r(b,d)&&c(b[d],d))break}function Q(b,c,d,i){c&&E(c,function(c,h){if(d||!r(b,h))i&&"string"!==typeof c?(b[h]||(b[h]={}),Q(b[h],
c,d,i)):b[h]=c});return b}function t(b,c){return function(){return c.apply(b,arguments)}}function Z(b){if(!b)return b;var c=Y;x(b.split("."),function(b){c=c[b]});return c}function J(b,c,d,i){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=i;d&&(c.originalError=d);return c}function ea(b){function c(a,g,v){var e,n,b,c,d,j,f,h=g&&g.split("/");e=h;var l=m.map,k=l&&l["*"];if(a&&"."===a.charAt(0))if(g){e=i(m.pkgs,g)?h=[g]:h.slice(0,h.length-1);g=a=e.concat(a.split("/"));
for(e=0;g[e];e+=1)if(n=g[e],"."===n)g.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===g[2]||".."===g[0]))break;else 0<e&&(g.splice(e-1,2),e-=2);e=i(m.pkgs,g=a[0]);a=a.join("/");e&&a===g+"/"+e.main&&(a=g)}else 0===a.indexOf("./")&&(a=a.substring(2));if(v&&(h||k)&&l){g=a.split("/");for(e=g.length;0<e;e-=1){b=g.slice(0,e).join("/");if(h)for(n=h.length;0<n;n-=1)if(v=i(l,h.slice(0,n).join("/")))if(v=i(v,b)){c=v;d=e;break}if(c)break;!j&&(k&&i(k,b))&&(j=i(k,b),f=e)}!c&&j&&(c=j,d=f);c&&(g.splice(0,d,
c),a=g.join("/"))}return a}function d(a){z&&x(document.getElementsByTagName("script"),function(g){if(g.getAttribute("data-requiremodule")===a&&g.getAttribute("data-requirecontext")===j.contextName)return g.parentNode.removeChild(g),!0})}function y(a){var g=i(m.paths,a);if(g&&I(g)&&1<g.length)return d(a),g.shift(),j.require.undef(a),j.require([a]),!0}function f(a){var g,b=a?a.indexOf("!"):-1;-1<b&&(g=a.substring(0,b),a=a.substring(b+1,a.length));return[g,a]}function h(a,g,b,e){var n,u,d=null,h=g?g.name:
null,l=a,m=!0,k="";a||(m=!1,a="_@r"+(L+=1));a=f(a);d=a[0];a=a[1];d&&(d=c(d,h,e),u=i(p,d));a&&(d?k=u&&u.normalize?u.normalize(a,function(a){return c(a,h,e)}):c(a,h,e):(k=c(a,h,e),a=f(k),d=a[0],k=a[1],b=!0,n=j.nameToUrl(k)));b=d&&!u&&!b?"_unnormalized"+(M+=1):"";return{prefix:d,name:k,parentMap:g,unnormalized:!!b,url:n,originalName:l,isDefine:m,id:(d?d+"!"+k:k)+b}}function q(a){var g=a.id,b=i(k,g);b||(b=k[g]=new j.Module(a));return b}function s(a,g,b){var e=a.id,n=i(k,e);if(r(p,e)&&(!n||n.defineEmitComplete))"defined"===
g&&b(p[e]);else q(a).on(g,b)}function C(a,g){var b=a.requireModules,e=!1;if(g)g(a);else if(x(b,function(g){if(g=i(k,g))g.error=a,g.events.error&&(e=!0,g.emit("error",a))}),!e)l.onError(a)}function w(){R.length&&(fa.apply(F,[F.length-1,0].concat(R)),R=[])}function A(a,g,b){var e=a.map.id;a.error?a.emit("error",a.error):(g[e]=!0,x(a.depMaps,function(e,c){var d=e.id,h=i(k,d);h&&(!a.depMatched[c]&&!b[d])&&(i(g,d)?(a.defineDep(c,p[d]),a.check()):A(h,g,b))}),b[e]=!0)}function B(){var a,g,b,e,n=(b=1E3*m.waitSeconds)&&
j.startTime+b<(new Date).getTime(),c=[],h=[],f=!1,l=!0;if(!T){T=!0;E(k,function(b){a=b.map;g=a.id;if(b.enabled&&(a.isDefine||h.push(b),!b.error))if(!b.inited&&n)y(g)?f=e=!0:(c.push(g),d(g));else if(!b.inited&&(b.fetched&&a.isDefine)&&(f=!0,!a.prefix))return l=!1});if(n&&c.length)return b=J("timeout","Load timeout for modules: "+c,null,c),b.contextName=j.contextName,C(b);l&&x(h,function(a){A(a,{},{})});if((!n||e)&&f)if((z||$)&&!U)U=setTimeout(function(){U=0;B()},50);T=!1}}function D(a){r(p,a[0])||
q(h(a[0],null,!0)).init(a[1],a[2])}function G(a){var a=a.currentTarget||a.srcElement,b=j.onScriptLoad;a.detachEvent&&!V?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=j.onScriptError;(!a.detachEvent||V)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function K(){var a;for(w();F.length;){a=F.shift();if(null===a[0])return C(J("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));D(a)}}var T,W,j,N,U,m={waitSeconds:7,
baseUrl:"./",paths:{},pkgs:{},shim:{},map:{},config:{}},k={},X={},F=[],p={},S={},L=1,M=1;N={require:function(a){return a.require?a.require:a.require=j.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=p[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m.config&&i(m.config,a.map.id)||{}},exports:p[a.map.id]}}};W=function(a){this.events=i(X,a.id)||{};this.map=a;this.shim=
i(m.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};W.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=t(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=
b)},fetch:function(){if(!this.fetched){this.fetched=!0;j.startTime=(new Date).getTime();var a=this.map;if(this.shim)j.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],t(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,j.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory;
if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&
!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0,
skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error=
a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a,
b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=
this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a,
b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments));
return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap;
m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k,
a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0<j;j-=1)if(k=
f.slice(0,j).join("/"),h=i(d,k),k=i(c,k)){I(k)&&(k=k[0]);f.splice(0,j,k);break}else if(h){c=a===h.name?h.location+"/"+h.main:h.location;f.splice(0,j,c);break}f=f.join("/");f+=b||(/\?/.test(f)?"":".js");f=("/"===f.charAt(0)||f.match(/^[\w\+\.\-]+:/)?"":m.baseUrl)+f}return m.urlArgs?f+((-1===f.indexOf("?")?"?":"&")+m.urlArgs):f},load:function(a,b){l.load(j,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ha.test((a.currentTarget||a.srcElement).readyState))P=
null,a=G(a),j.completeLoad(a.id)},onScriptError:function(a){var b=G(a);if(!y(b.id))return C(J("scripterror","Script error",a,[b.id]))}};j.require=j.makeRequire();return j}var l,w,A,D,s,G,P,K,ba,ca,ia=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ja=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,aa=/\.js$/,ga=/^\.\//;w=Object.prototype;var L=w.toString,da=w.hasOwnProperty,fa=Array.prototype.splice,z=!!("undefined"!==typeof window&&navigator&&document),$=!z&&"undefined"!==typeof importScripts,ha=z&&
"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,V="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),B={},q={},R=[],O=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(H(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!H(require)&&(q=require,require=void 0);l=requirejs=function(b,c,d,y){var f,h="_";!I(b)&&"string"!==typeof b&&(f=b,I(c)?(b=c,c=d,d=y):b=[]);f&&f.context&&(h=f.context);(y=i(B,h))||(y=B[h]=l.s.newContext(h));
f&&y.configure(f);return y.require(b,c,d)};l.config=function(b){return l(b)};l.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=l);l.version="2.1.2";l.jsExtRegExp=/^\/|:|\?|\.js$/;l.isBrowser=z;w=l.s={contexts:B,newContext:ea};l({});x(["toUrl","undef","defined","specified"],function(b){l[b]=function(){var c=B._;return c.require[b].apply(c,arguments)}});if(z&&(A=w.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))A=
w.head=D.parentNode;l.onError=function(b){throw b;};l.load=function(b,c,d){var i=b&&b.config||{},f;if(z)return f=i.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),f.type=i.scriptType||"text/javascript",f.charset="utf-8",f.async=!0,f.setAttribute("data-requirecontext",b.contextName),f.setAttribute("data-requiremodule",c),f.attachEvent&&!(f.attachEvent.toString&&0>f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange",
b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i,
f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd=
{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this);
// The object which will be globally available via RequireJS variable.
return {
'requirejs': requirejs,
'require': require,
'define': define
};
}(); // End-of: var RequireJS = function()
# Testing
Testing is good. Here is some useful info about how we set up tests--
Testing is good. Here is some useful info about how we set up tests.
More info is [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Test+Engineering)
### Backend code:
## Backend code
- TODO
- The python unit tests can be run via rake tasks.
See development.md for more info on how to do this.
### Frontend code:
## Frontend code
We're using Jasmine to unit-testing the JavaScript files. All the specs are
written in CoffeeScript for the consistency. To access the test cases, start the
server in debug mode, navigate to `http://127.0.0.1:[port number]/_jasmine` to
see the test result.
### Jasmine
We're using Jasmine to unit/integration test the JavaScript files.
More info [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Jasmine)
All the specs are written in CoffeeScript to be consistent with the code.
To access the test cases, start the server using the settings file **jasmine.py** using this command:
`rake django-admin[runserver,lms,jasmine,12345]`
Then navigate to `http://localhost:12345/_jasmine/` to see the test results.
All the JavaScript codes must have test coverage. Both CMS and LMS
has its own test directory in `{cms,lms}/static/coffee/spec` If you haven't
......@@ -30,3 +38,31 @@ If you're finishing a feature that contains JavaScript code snippets and do not
sure how to test, please feel free to open up a pull request and asking people
for help. (However, the best way to do it would be writing your test first, then
implement your feature - Test Driven Development.)
### BDD style acceptance tests with Lettuce
We're using Lettuce for end user acceptance testing of features.
More info [on the wiki](https://edx-wiki.atlassian.net/wiki/display/ENG/Lettuce+Acceptance+Testing)
Lettuce is a port of Cucumber. We're using it to drive Splinter, which is a python wrapper to Selenium.
To execute the automated test scripts, you'll need to start up the django server separately, then launch the tests.
Do both use the settings file named **acceptance.py**.
What this will do is to use a sqllite database named mitx_all/db/test_mitx.db.
That way it can be flushed etc. without messing up your dev db.
Note that this also means that you need to syncdb and migrate the db first before starting the server to initialize it if it does not yet exist.
1. Set up the test database (only needs to be done once):
rm ../db/test_mitx.db
rake django-admin[syncdb,lms,acceptance,--noinput]
rake django-admin[migrate,lms,acceptance,--noinput]
2. Start up the django server separately in a shell
rake lms[acceptance]
3. Then in another shell, run the tests in different ways as below. Lettuce comes with a new django-admin command called _harvest_. See the [lettuce django docs](http://lettuce.it/recipes/django-lxml.html) for more details.
* All tests in a specified feature folder: `django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/portal/features/`
* Only the specified feature's scenarios: `django-admin.py harvest --no-server --settings=lms.envs.acceptance --pythonpath=. lms/djangoapps/courseware/features/high-level-tabs.feature`
4. Troubleshooting
* If you get an error msg that says something about harvest not being a command, you probably are missing a requirement. Pip install (test-requirements.txt) and/or brew install as needed.
\ No newline at end of file
from lettuce import world
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from courseware.courses import get_course_by_id
from xmodule import seq_module, vertical_module
from logging import getLogger
logger = getLogger(__name__)
## support functions
def get_courses():
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number.
'''
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
return courses
# def get_courseware(course_id):
# """
# Given a course_id (string), return a courseware array of dictionaries for the
# top two levels of navigation. Example:
# [
# {'chapter_name': 'Overview',
# 'sections': ['Welcome', 'System Usage Sequence', 'Lab0: Using the tools', 'Circuit Sandbox']
# },
# {'chapter_name': 'Week 1',
# 'sections': ['Administrivia and Circuit Elements', 'Basic Circuit Analysis', 'Resistor Divider', 'Week 1 Tutorials']
# },
# {'chapter_name': 'Midterm Exam',
# 'sections': ['Midterm Exam']
# }
# ]
# """
# course = get_course_by_id(course_id)
# chapters = course.get_children()
# courseware = [ {'chapter_name':c.display_name, 'sections':[s.display_name for s in c.get_children()]} for c in chapters]
# return courseware
def get_courseware_with_tabs(course_id):
"""
Given a course_id (string), return a courseware array of dictionaries for the
top three levels of navigation. Same as get_courseware() except include
the tabs on the right hand main navigation page.
This hides the appropriate courseware as defined by the XML flag test:
chapter.metadata.get('hide_from_toc','false').lower() == 'true'
Example:
[{
'chapter_name': 'Overview',
'sections': [{
'clickable_tab_count': 0,
'section_name': 'Welcome',
'tab_classes': []
}, {
'clickable_tab_count': 1,
'section_name': 'System Usage Sequence',
'tab_classes': ['VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Lab0: Using the tools',
'tab_classes': ['HtmlDescriptor', 'HtmlDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Circuit Sandbox',
'tab_classes': []
}]
}, {
'chapter_name': 'Week 1',
'sections': [{
'clickable_tab_count': 4,
'section_name': 'Administrivia and Circuit Elements',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Basic Circuit Analysis',
'tab_classes': ['CapaDescriptor', 'CapaDescriptor', 'CapaDescriptor']
}, {
'clickable_tab_count': 0,
'section_name': 'Resistor Divider',
'tab_classes': []
}, {
'clickable_tab_count': 0,
'section_name': 'Week 1 Tutorials',
'tab_classes': []
}]
}, {
'chapter_name': 'Midterm Exam',
'sections': [{
'clickable_tab_count': 2,
'section_name': 'Midterm Exam',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
}]
}]
"""
course = get_course_by_id(course_id)
chapters = [ chapter for chapter in course.get_children() if chapter.metadata.get('hide_from_toc','false').lower() != 'true' ]
courseware = [{'chapter_name':c.display_name,
'sections':[{'section_name':s.display_name,
'clickable_tab_count':len(s.get_children()) if (type(s)==seq_module.SequenceDescriptor) else 0,
'tabs':[{'children_count':len(t.get_children()) if (type(t)==vertical_module.VerticalDescriptor) else 0,
'class':t.__class__.__name__ }
for t in s.get_children() ]}
for s in c.get_children() if s.metadata.get('hide_from_toc', 'false').lower() != 'true']}
for c in chapters ]
return courseware
def process_section(element, num_tabs=0):
'''
Process section reads through whatever is in 'course-content' and classifies it according to sequence module type.
This function is recursive
There are 6 types, with 6 actions.
Sequence Module
-contains one child module
Vertical Module
-contains other modules
-process it and get its children, then process them
Capa Module
-problem type, contains only one problem
-for this, the most complex type, we created a separate method, process_problem
Video Module
-video type, contains only one video
-we only check to ensure that a section with class of video exists
HTML Module
-html text
-we do not check anything about it
Custom Tag Module
-a custom 'hack' module type
-there is a large variety of content that could go in a custom tag module, so we just pass if it is of this unusual type
can be used like this:
e = world.browser.find_by_css('section.course-content section')
process_section(e)
'''
if element.has_class('xmodule_display xmodule_SequenceModule'):
logger.debug('####### Processing xmodule_SequenceModule')
child_modules = element.find_by_css("div>div>section[class^='xmodule']")
for mod in child_modules:
process_section(mod)
elif element.has_class('xmodule_display xmodule_VerticalModule'):
logger.debug('####### Processing xmodule_VerticalModule')
vert_list = element.find_by_css("li section[class^='xmodule']")
for item in vert_list:
process_section(item)
elif element.has_class('xmodule_display xmodule_CapaModule'):
logger.debug('####### Processing xmodule_CapaModule')
assert element.find_by_css("section[id^='problem']"), "No problems found in Capa Module"
p = element.find_by_css("section[id^='problem']").first
p_id = p['id']
logger.debug('####################')
logger.debug('id is "%s"' % p_id)
logger.debug('####################')
process_problem(p, p_id)
elif element.has_class('xmodule_display xmodule_VideoModule'):
logger.debug('####### Processing xmodule_VideoModule')
assert element.find_by_css("section[class^='video']"), "No video found in Video Module"
elif element.has_class('xmodule_display xmodule_HtmlModule'):
logger.debug('####### Processing xmodule_HtmlModule')
pass
elif element.has_class('xmodule_display xmodule_CustomTagModule'):
logger.debug('####### Processing xmodule_CustomTagModule')
pass
else:
assert False, "Class for element not recognized!!"
def process_problem(element, problem_id):
'''
Process problem attempts to
1) scan all the input fields and reset them
2) click the 'check' button and look for an incorrect response (p.status text should be 'incorrect')
3) click the 'show answer' button IF it exists and IF the answer is not already displayed
4) enter the correct answer in each input box
5) click the 'check' button and verify that answers are correct
Because of all the ajax calls happening, sometimes the test fails because objects disconnect from the DOM.
The basic functionality does exist, though, and I'm hoping that someone can take it over and make it super effective.
'''
prob_xmod = element.find_by_css("section.problem").first
input_fields = prob_xmod.find_by_css("section[id^='input']")
## clear out all input to ensure an incorrect result
for field in input_fields:
field.find_by_css("input").first.fill('')
## because of cookies or the application, only click the 'check' button if the status is not already 'incorrect'
# This would need to be reworked because multiple choice problems don't have this status
# if prob_xmod.find_by_css("p.status").first.text.strip().lower() != 'incorrect':
prob_xmod.find_by_css("section.action input.check").first.click()
## all elements become disconnected after the click
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
# Wait for the ajax reload
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
prob_xmod = element.find_by_css("section.problem").first
input_fields = prob_xmod.find_by_css("section[id^='input']")
for field in input_fields:
assert field.find_by_css("div.incorrect"), "The 'check' button did not work for %s" % (problem_id)
show_button = element.find_by_css("section.action input.show").first
## this logic is to ensure we do not accidentally hide the answers
if show_button.value.lower() == 'show answer':
show_button.click()
else:
pass
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
prob_xmod = element.find_by_css("section.problem").first
input_fields = prob_xmod.find_by_css("section[id^='input']")
## in each field, find the answer, and send it to the field.
## Note that this does not work if the answer type is a strange format, e.g. "either a or b"
for field in input_fields:
field.find_by_css("input").first.fill(field.find_by_css("p[id^='answer']").first.text)
prob_xmod.find_by_css("section.action input.check").first.click()
## assert that we entered the correct answers
## grab element and prob_xmod because the dom has changed (some classes/elements became hidden and changed the hierarchy)
assert world.browser.is_element_present_by_css("section[id='%s']" % problem_id, wait_time=5)
element = world.browser.find_by_css("section[id='%s']" % problem_id).first
prob_xmod = element.find_by_css("section.problem").first
input_fields = prob_xmod.find_by_css("section[id^='input']")
for field in input_fields:
## if you don't use 'starts with ^=' the test will fail because the actual class is 'correct ' (with a space)
assert field.find_by_css("div[class^='correct']"), "The check answer values were not correct for %s" % problem_id
Feature: View the Courseware Tab
As a student in an edX course
In order to work on the course
I want to view the info on the courseware tab
Scenario: I can get to the courseware tab when logged in
Given I am registered for a course
And I log in
And I click on View Courseware
When I click on the "Courseware" tab
Then the "Courseware" tab is active
# TODO: fix this one? Not sure whether you should get a 404.
# Scenario: I cannot get to the courseware tab when not logged in
# Given I am not logged in
# And I visit the homepage
# When I visit the courseware URL
# Then the login dialog is visible
from lettuce import world, step
from lettuce.django import django_url
@step('I visit the courseware URL$')
def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
\ No newline at end of file
from lettuce import world, step
from lettuce.django import django_url
@step('I click on View Courseware')
def i_click_on_view_courseware(step):
css = 'p.enter-course'
world.browser.find_by_css(css).first.click()
@step('I click on the "([^"]*)" tab$')
def i_click_on_the_tab(step, tab):
world.browser.find_link_by_text(tab).first.click()
world.save_the_html()
@step('I visit the courseware URL$')
def i_visit_the_course_info_url(step):
url = django_url('/courses/MITx/6.002x/2012_Fall/courseware')
world.browser.visit(url)
@step(u'I do not see "([^"]*)" anywhere on the page')
def i_do_not_see_text_anywhere_on_the_page(step, text):
assert world.browser.is_text_not_present(text)
@step(u'I am on the dashboard page$')
def i_am_on_the_dashboard_page(step):
assert world.browser.is_element_present_by_css('section.courses')
assert world.browser.url == django_url('/dashboard')
@step('the "([^"]*)" tab is active$')
def the_tab_is_active(step, tab):
css = '.course-tabs a.active'
active_tab = world.browser.find_by_css(css)
assert (active_tab.text == tab)
@step('the login dialog is visible$')
def login_dialog_visible(step):
css = 'form#login_form.login_form'
assert world.browser.find_by_css(css).visible
Feature: All the high level tabs should work
In order to preview the courseware
As a student
I want to navigate through the high level tabs
# Note this didn't work as a scenario outline because
# before each scenario was not flushing the database
# TODO: break this apart so that if one fails the others
# will still run
Scenario: A student can see all tabs of the course
Given I am registered for a course
And I log in
And I click on View Courseware
When I click on the "Courseware" tab
Then the page title should be "6.002x Courseware"
When I click on the "Course Info" tab
Then the page title should be "6.002x Course Info"
When I click on the "Textbook" tab
Then the page title should be "6.002x Textbook"
When I click on the "Wiki" tab
Then the page title should be "6.002x | edX Wiki"
When I click on the "Progress" tab
Then the page title should be "6.002x Progress"
Feature: Open ended grading
As a student in an edX course
In order to complete the courseware questions
I want the machine learning grading to be functional
Scenario: An answer that is too short is rejected
Given I navigate to an openended question
And I enter the answer "z"
When I press the "Check" button
And I wait for "8" seconds
And I see the grader status "Submitted for grading"
And I press the "Recheck for Feedback" button
Then I see the red X
And I see the grader score "0"
Scenario: An answer with too many spelling errors is rejected
Given I navigate to an openended question
And I enter the answer "az"
When I press the "Check" button
And I wait for "8" seconds
And I see the grader status "Submitted for grading"
And I press the "Recheck for Feedback" button
Then I see the red X
And I see the grader score "0"
When I click the link for full output
Then I see the spelling grading message "More spelling errors than average."
Scenario: An answer makes its way to the instructor dashboard
Given I navigate to an openended question as staff
When I submit the answer "I love Chemistry."
And I wait for "8" seconds
And I visit the staff grading page
Then my answer is queued for instructor grading
\ No newline at end of file
from lettuce import world, step
from lettuce.django import django_url
from nose.tools import assert_equals, assert_in
from logging import getLogger
logger = getLogger(__name__)
@step('I navigate to an openended question$')
def navigate_to_an_openended_question(step):
world.register_by_course_id('MITx/3.091x/2012_Fall')
world.log_in('robot@edx.org','test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
@step('I navigate to an openended question as staff$')
def navigate_to_an_openended_question_as_staff(step):
world.register_by_course_id('MITx/3.091x/2012_Fall', True)
world.log_in('robot@edx.org','test')
problem = '/courses/MITx/3.091x/2012_Fall/courseware/Week_10/Polymer_Synthesis/'
world.browser.visit(django_url(problem))
tab_css = 'ol#sequence-list > li > a[data-element="5"]'
world.browser.find_by_css(tab_css).click()
@step(u'I enter the answer "([^"]*)"$')
def enter_the_answer_text(step, text):
textarea_css = 'textarea'
world.browser.find_by_css(textarea_css).first.fill(text)
@step(u'I submit the answer "([^"]*)"$')
def i_submit_the_answer_text(step, text):
textarea_css = 'textarea'
world.browser.find_by_css(textarea_css).first.fill(text)
check_css = 'input.check'
world.browser.find_by_css(check_css).click()
@step('I click the link for full output$')
def click_full_output_link(step):
link_css = 'a.full'
world.browser.find_by_css(link_css).first.click()
@step(u'I visit the staff grading page$')
def i_visit_the_staff_grading_page(step):
# course_u = '/courses/MITx/3.091x/2012_Fall'
# sg_url = '%s/staff_grading' % course_u
world.browser.click_link_by_text('Instructor')
world.browser.click_link_by_text('Staff grading')
# world.browser.visit(django_url(sg_url))
@step(u'I see the grader message "([^"]*)"$')
def see_grader_message(step, msg):
message_css = 'div.external-grader-message'
grader_msg = world.browser.find_by_css(message_css).text
assert_in(msg, grader_msg)
@step(u'I see the grader status "([^"]*)"$')
def see_the_grader_status(step, status):
status_css = 'div.grader-status'
grader_status = world.browser.find_by_css(status_css).text
assert_equals(status, grader_status)
@step('I see the red X$')
def see_the_red_x(step):
x_css = 'div.grader-status > span.incorrect'
assert world.browser.find_by_css(x_css)
@step(u'I see the grader score "([^"]*)"$')
def see_the_grader_score(step, score):
score_css = 'div.result-output > p'
score_text = world.browser.find_by_css(score_css).text
assert_equals(score_text, 'Score: %s' % score)
@step('I see the link for full output$')
def see_full_output_link(step):
link_css = 'a.full'
assert world.browser.find_by_css(link_css)
@step('I see the spelling grading message "([^"]*)"$')
def see_spelling_msg(step, msg):
spelling_css = 'div.spelling'
spelling_msg = world.browser.find_by_css(spelling_css).text
assert_equals('Spelling: %s' % msg, spelling_msg)
@step(u'my answer is queued for instructor grading$')
def answer_is_queued_for_instructor_grading(step):
list_css = 'ul.problem-list > li > a'
actual_msg = world.browser.find_by_css(list_css).text
expected_msg = "(0 graded, 1 pending)"
assert_in(expected_msg, actual_msg)
# Here are all the courses for Fall 2012
# MITx/3.091x/2012_Fall
# MITx/6.002x/2012_Fall
# MITx/6.00x/2012_Fall
# HarvardX/CS50x/2012 (we will not be testing this, as it is anomolistic)
# HarvardX/PH207x/2012_Fall
# BerkeleyX/CS169.1x/2012_Fall
# BerkeleyX/CS169.2x/2012_Fall
# BerkeleyX/CS184.1x/2012_Fall
#You can load the courses into your data directory with these cmds:
# git clone https://github.com/MITx/3.091x.git
# git clone https://github.com/MITx/6.00x.git
# git clone https://github.com/MITx/content-mit-6002x.git
# git clone https://github.com/MITx/content-mit-6002x.git
# git clone https://github.com/MITx/content-harvard-id270x.git
# git clone https://github.com/MITx/content-berkeley-cs169x.git
# git clone https://github.com/MITx/content-berkeley-cs169.2x.git
# git clone https://github.com/MITx/content-berkeley-cs184x.git
Feature: There are courses on the homepage
In order to compared rendered content to the database
As an acceptance test
I want to count all the chapters, sections, and tabs for each course
Scenario: Navigate through course MITx/3.091x/2012_Fall
Given I am registered for course "MITx/3.091x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course MITx/6.002x/2012_Fall
Given I am registered for course "MITx/6.002x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course MITx/6.00x/2012_Fall
Given I am registered for course "MITx/6.00x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course HarvardX/PH207x/2012_Fall
Given I am registered for course "HarvardX/PH207x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall
Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall
Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall"
And I log in
Then I verify all the content of each course
Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
And I log in
Then I verify all the content of each course
\ No newline at end of file
from lettuce import world, step
from re import sub
from nose.tools import assert_equals
from xmodule.modulestore.django import modulestore
from courses import *
from logging import getLogger
logger = getLogger(__name__)
def check_for_errors():
e = world.browser.find_by_css('.outside-app')
if len(e) > 0:
assert False, 'there was a server error at %s' % (world.browser.url)
else:
assert True
@step(u'I verify all the content of each course')
def i_verify_all_the_content_of_each_course(step):
all_possible_courses = get_courses()
logger.debug('Courses found:')
for c in all_possible_courses:
logger.debug(c.id)
ids = [c.id for c in all_possible_courses]
# Get a list of all the registered courses
registered_courses = world.browser.find_by_css('article.my-course')
if len(all_possible_courses) < len(registered_courses):
assert False, "user is registered for more courses than are uniquely posssible"
else:
pass
for test_course in registered_courses:
test_course.find_by_css('a').click()
check_for_errors()
# Get the course. E.g. 'MITx/6.002x/2012_Fall'
current_course = sub('/info','', sub('.*/courses/', '', world.browser.url))
validate_course(current_course,ids)
world.browser.find_link_by_text('Courseware').click()
assert world.browser.is_element_present_by_id('accordion',wait_time=2)
check_for_errors()
browse_course(current_course)
# clicking the user link gets you back to the user's home page
world.browser.find_by_css('.user-link').click()
check_for_errors()
def browse_course(course_id):
## count chapters from xml and page and compare
chapters = get_courseware_with_tabs(course_id)
num_chapters = len(chapters)
rendered_chapters = world.browser.find_by_css('#accordion > nav > div')
num_rendered_chapters = len(rendered_chapters)
msg = '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id)
#logger.debug(msg)
assert num_chapters == num_rendered_chapters, msg
chapter_it = 0
## Iterate the chapters
while chapter_it < num_chapters:
## click into a chapter
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click()
## look for the "there was a server error" div
check_for_errors()
## count sections from xml and page and compare
sections = chapters[chapter_it]['sections']
num_sections = len(sections)
rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')
num_rendered_sections = len(rendered_sections)
msg = ('%d sections expected, %d sections found on page, %s - %d - %s' %
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
#logger.debug(msg)
assert num_sections == num_rendered_sections, msg
section_it = 0
## Iterate the sections
while section_it < num_sections:
## click on a section
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click()
## sometimes the course-content takes a long time to load
assert world.browser.is_element_present_by_css('.course-content',wait_time=5)
## look for server error div
check_for_errors()
## count tabs from xml and page and compare
## count the number of tabs. If number of tabs is 0, there won't be anything rendered
## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length
num_tabs = sections[section_it]['clickable_tab_count']
if num_tabs != 0:
rendered_tabs = world.browser.find_by_css('ol#sequence-list > li')
num_rendered_tabs = len(rendered_tabs)
else:
rendered_tabs = 0
num_rendered_tabs = 0
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
#logger.debug(msg)
# Save the HTML to a file for later comparison
world.save_the_course_content('/tmp/%s' % course_id)
assert num_tabs == num_rendered_tabs, msg
tabs = sections[section_it]['tabs']
tab_it = 0
## Iterate the tabs
while tab_it < num_tabs:
rendered_tabs[tab_it].find_by_tag('a').click()
## do something with the tab sections[section_it]
# e = world.browser.find_by_css('section.course-content section')
# process_section(e)
tab_children = tabs[tab_it]['children_count']
tab_class = tabs[tab_it]['class']
if tab_children != 0:
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
num_rendered_items = len(rendered_items)
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
#logger.debug(msg)
assert tab_children == num_rendered_items, msg
tab_it += 1
section_it += 1
chapter_it += 1
def validate_course(current_course, ids):
try:
ids.index(current_course)
except:
assert False, "invalid course id %s" % current_course
## acceptance_testing
This fake django app is here to support acceptance testing using <a href="http://lettuce.it/">lettuce</a> +
<a href="http://splinter.cobrateam.info/">splinter</a> (which wraps <a href="http://selenium.googlecode.com/svn/trunk/docs/api/py/index.html">selenium</a>).
First you need to make sure that you've installed the requirements.
This includes lettuce, selenium, splinter, etc.
Do this with:
```pip install -r test-requirements.txt```
The settings.py environment file used is named acceptance.py.
It uses a test SQLite database defined as ../db/test-mitx.db.
You need to first start up the server separately, then run the lettuce scenarios.
Full documentation can be found on the wiki at <a href="https://edx-wiki.atlassian.net/wiki/display/ENG/Lettuce+Acceptance+Testing">this link</a>.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment