Merge branch 'master' into diana/peer-grading-views
This commit is contained in:
@@ -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) =>
|
||||
|
||||
57
common/static/js/vendor/RequireJS.js
vendored
Normal file
57
common/static/js/vendor/RequireJS.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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()
|
||||
@@ -1,17 +1,25 @@
|
||||
# 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.
|
||||
254
lms/djangoapps/courseware/features/courses.py
Normal file
254
lms/djangoapps/courseware/features/courses.py
Normal file
@@ -0,0 +1,254 @@
|
||||
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
|
||||
18
lms/djangoapps/courseware/features/courseware.feature
Normal file
18
lms/djangoapps/courseware/features/courseware.feature
Normal file
@@ -0,0 +1,18 @@
|
||||
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
|
||||
7
lms/djangoapps/courseware/features/courseware.py
Normal file
7
lms/djangoapps/courseware/features/courseware.py
Normal file
@@ -0,0 +1,7 @@
|
||||
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)
|
||||
37
lms/djangoapps/courseware/features/courseware_common.py
Normal file
37
lms/djangoapps/courseware/features/courseware_common.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
23
lms/djangoapps/courseware/features/high-level-tabs.feature
Normal file
23
lms/djangoapps/courseware/features/high-level-tabs.feature
Normal file
@@ -0,0 +1,23 @@
|
||||
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"
|
||||
33
lms/djangoapps/courseware/features/openended.feature
Normal file
33
lms/djangoapps/courseware/features/openended.feature
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
89
lms/djangoapps/courseware/features/openended.py
Normal file
89
lms/djangoapps/courseware/features/openended.py
Normal file
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
59
lms/djangoapps/courseware/features/smart-accordion.feature
Normal file
59
lms/djangoapps/courseware/features/smart-accordion.feature
Normal file
@@ -0,0 +1,59 @@
|
||||
# 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
|
||||
152
lms/djangoapps/courseware/features/smart-accordion.py
Normal file
152
lms/djangoapps/courseware/features/smart-accordion.py
Normal file
@@ -0,0 +1,152 @@
|
||||
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
|
||||
15
lms/djangoapps/portal/README.md
Normal file
15
lms/djangoapps/portal/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## 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
lms/djangoapps/portal/__init__.py
Normal file
0
lms/djangoapps/portal/__init__.py
Normal file
84
lms/djangoapps/portal/features/common.py
Normal file
84
lms/djangoapps/portal/features/common.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from lettuce import world, step#, before, after
|
||||
from factories import *
|
||||
from django.core.management import call_command
|
||||
from nose.tools import assert_equals, assert_in
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
import time
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@step(u'I wait (?:for )?"(\d+)" seconds?$')
|
||||
def wait(step, seconds):
|
||||
time.sleep(float(seconds))
|
||||
|
||||
@step('I (?:visit|access|open) the homepage$')
|
||||
def i_visit_the_homepage(step):
|
||||
world.browser.visit(django_url('/'))
|
||||
assert world.browser.is_element_present_by_css('header.global', 10)
|
||||
|
||||
@step(u'I (?:visit|access|open) the dashboard$')
|
||||
def i_visit_the_dashboard(step):
|
||||
world.browser.visit(django_url('/dashboard'))
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
|
||||
@step(r'click (?:the|a) link (?:called|with the text) "([^"]*)"$')
|
||||
def click_the_link_called(step, text):
|
||||
world.browser.find_link_by_text(text).click()
|
||||
|
||||
@step('I should be on the dashboard page$')
|
||||
def i_should_be_on_the_dashboard(step):
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
assert world.browser.title == 'Dashboard'
|
||||
|
||||
@step(u'I (?:visit|access|open) the courses page$')
|
||||
def i_am_on_the_courses_page(step):
|
||||
world.browser.visit(django_url('/courses'))
|
||||
assert world.browser.is_element_present_by_css('section.courses')
|
||||
|
||||
@step('I should see that the path is "([^"]*)"$')
|
||||
def i_should_see_that_the_path_is(step, path):
|
||||
assert world.browser.url == django_url(path)
|
||||
|
||||
@step(u'the page title should be "([^"]*)"$')
|
||||
def the_page_title_should_be(step, title):
|
||||
assert world.browser.title == title
|
||||
|
||||
@step(r'should see that the url is "([^"]*)"$')
|
||||
def should_have_the_url(step, url):
|
||||
assert_equals(world.browser.url, url)
|
||||
|
||||
@step(r'should see (?:the|a) link (?:called|with the text) "([^"]*)"$')
|
||||
def should_see_a_link_called(step, text):
|
||||
assert len(world.browser.find_link_by_text(text)) > 0
|
||||
|
||||
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
|
||||
def should_see_in_the_page(step, text):
|
||||
assert_in(text, world.browser.html)
|
||||
|
||||
@step('I am logged in$')
|
||||
def i_am_logged_in(step):
|
||||
world.create_user('robot')
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
|
||||
@step('I am not logged in$')
|
||||
def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
@step(u'I am registered for a course$')
|
||||
def i_am_registered_for_a_course(step):
|
||||
world.create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall')
|
||||
world.log_in('robot@edx.org', 'test')
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
def i_am_an_edx_user(step):
|
||||
world.create_user('robot')
|
||||
|
||||
@step(u'User "([^"]*)" is an edX user$')
|
||||
def registered_edx_user(step, uname):
|
||||
world.create_user(uname)
|
||||
34
lms/djangoapps/portal/features/factories.py
Normal file
34
lms/djangoapps/portal/features/factories.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import factory
|
||||
from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Jack Foo'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
47
lms/djangoapps/portal/features/homepage.feature
Normal file
47
lms/djangoapps/portal/features/homepage.feature
Normal file
@@ -0,0 +1,47 @@
|
||||
Feature: Homepage for web users
|
||||
In order to get an idea what edX is about
|
||||
As a an anonymous web user
|
||||
I want to check the information on the home page
|
||||
|
||||
Scenario: User can see the "Login" button
|
||||
Given I visit the homepage
|
||||
Then I should see a link called "Log In"
|
||||
|
||||
Scenario: User can see the "Sign up" button
|
||||
Given I visit the homepage
|
||||
Then I should see a link called "Sign Up"
|
||||
|
||||
Scenario Outline: User can see main parts of the page
|
||||
Given I visit the homepage
|
||||
Then I should see a link called "<Link>"
|
||||
When I click the link with the text "<Link>"
|
||||
Then I should see that the path is "<Path>"
|
||||
|
||||
Examples:
|
||||
| Link | Path |
|
||||
| Find Courses | /courses |
|
||||
| About | /about |
|
||||
| Jobs | /jobs |
|
||||
| Contact | /contact |
|
||||
|
||||
Scenario: User can visit the blog
|
||||
Given I visit the homepage
|
||||
When I click the link with the text "Blog"
|
||||
Then I should see that the url is "http://blog.edx.org/"
|
||||
|
||||
# TODO: test according to domain or policy
|
||||
Scenario: User can see the partner institutions
|
||||
Given I visit the homepage
|
||||
Then I should see "<Partner>" in the Partners section
|
||||
|
||||
Examples:
|
||||
| Partner |
|
||||
| MITx |
|
||||
| HarvardX |
|
||||
| BerkeleyX |
|
||||
| UTx |
|
||||
| WellesleyX |
|
||||
| GeorgetownX |
|
||||
|
||||
# # TODO: Add scenario that tests the courses available
|
||||
# # using a policy or a configuration file
|
||||
8
lms/djangoapps/portal/features/homepage.py
Normal file
8
lms/djangoapps/portal/features/homepage.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in
|
||||
|
||||
@step('I should see "([^"]*)" in the Partners section$')
|
||||
def i_should_see_partner(step, partner):
|
||||
partners = world.browser.find_by_css(".partner .name span")
|
||||
names = set(span.text for span in partners)
|
||||
assert_in(partner, names)
|
||||
27
lms/djangoapps/portal/features/login.feature
Normal file
27
lms/djangoapps/portal/features/login.feature
Normal file
@@ -0,0 +1,27 @@
|
||||
Feature: Login in as a registered user
|
||||
As a registered user
|
||||
In order to access my content
|
||||
I want to be able to login in to edX
|
||||
|
||||
Scenario: Login to an unactivated account
|
||||
Given I am an edX user
|
||||
And I am an unactivated user
|
||||
And I visit the homepage
|
||||
When I click the link with the text "Log In"
|
||||
And I submit my credentials on the login form
|
||||
Then I should see the login error message "This account has not been activated"
|
||||
|
||||
Scenario: Login to an activated account
|
||||
Given I am an edX user
|
||||
And I am an activated user
|
||||
And I visit the homepage
|
||||
When I click the link with the text "Log In"
|
||||
And I submit my credentials on the login form
|
||||
Then I should be on the dashboard page
|
||||
|
||||
Scenario: Logout of a signed in account
|
||||
Given I am logged in
|
||||
When I click the dropdown arrow
|
||||
And I click the link with the text "Log Out"
|
||||
Then I should see a link with the text "Log In"
|
||||
And I should see that the path is "/"
|
||||
45
lms/djangoapps/portal/features/login.py
Normal file
45
lms/djangoapps/portal/features/login.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from lettuce import step, world
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@step('I am an unactivated user$')
|
||||
def i_am_an_unactivated_user(step):
|
||||
user_is_an_unactivated_user('robot')
|
||||
|
||||
@step('I am an activated user$')
|
||||
def i_am_an_activated_user(step):
|
||||
user_is_an_activated_user('robot')
|
||||
|
||||
@step('I submit my credentials on the login form')
|
||||
def i_submit_my_credentials_on_the_login_form(step):
|
||||
fill_in_the_login_form('email', 'robot@edx.org')
|
||||
fill_in_the_login_form('password', 'test')
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_value('Access My Courses').click()
|
||||
|
||||
@step(u'I should see the login error message "([^"]*)"$')
|
||||
def i_should_see_the_login_error_message(step, msg):
|
||||
login_error_div = world.browser.find_by_css('form#login_form #login_error')
|
||||
assert (msg in login_error_div.text)
|
||||
|
||||
@step(u'click the dropdown arrow$')
|
||||
def click_the_dropdown(step):
|
||||
css = ".dropdown"
|
||||
e = world.browser.find_by_css(css)
|
||||
e.click()
|
||||
|
||||
#### helper functions
|
||||
|
||||
def user_is_an_unactivated_user(uname):
|
||||
u = User.objects.get(username=uname)
|
||||
u.is_active = False
|
||||
u.save()
|
||||
|
||||
def user_is_an_activated_user(uname):
|
||||
u = User.objects.get(username=uname)
|
||||
u.is_active = True
|
||||
u.save()
|
||||
|
||||
def fill_in_the_login_form(field, value):
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
form_field = login_form.find_by_name(field)
|
||||
form_field.fill(value)
|
||||
17
lms/djangoapps/portal/features/registration.feature
Normal file
17
lms/djangoapps/portal/features/registration.feature
Normal file
@@ -0,0 +1,17 @@
|
||||
Feature: Register for a course
|
||||
As a registered user
|
||||
In order to access my class content
|
||||
I want to register for a class on the edX website
|
||||
|
||||
Scenario: I can register for a course
|
||||
Given I am logged in
|
||||
And I visit the courses page
|
||||
When I register for the course numbered "6.002x"
|
||||
Then I should see the course numbered "6.002x" in my dashboard
|
||||
|
||||
Scenario: I can unregister for a course
|
||||
Given I am registered for a course
|
||||
And I visit the dashboard
|
||||
When I click the link with the text "Unregister"
|
||||
And I press the "Unregister" button in the Unenroll dialog
|
||||
Then I should see "Looks like you haven't registered for any courses yet." somewhere in the page
|
||||
24
lms/djangoapps/portal/features/registration.py
Normal file
24
lms/djangoapps/portal/features/registration.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from lettuce import world, step
|
||||
|
||||
@step('I register for the course numbered "([^"]*)"$')
|
||||
def i_register_for_the_course(step, course):
|
||||
courses_section = world.browser.find_by_css('section.courses')
|
||||
course_link_css = 'article[id*="%s"] a' % course
|
||||
course_link = courses_section.find_by_css(course_link_css).first
|
||||
course_link.click()
|
||||
|
||||
intro_section = world.browser.find_by_css('section.intro')
|
||||
register_link = intro_section.find_by_css('a.register')
|
||||
register_link.click()
|
||||
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard')
|
||||
|
||||
@step(u'I should see the course numbered "([^"]*)" in my dashboard$')
|
||||
def i_should_see_that_course_in_my_dashboard(step, course):
|
||||
course_link_css = 'section.my-courses a[href*="%s"]' % course
|
||||
assert world.browser.is_element_present_by_css(course_link_css)
|
||||
|
||||
@step(u'I press the "([^"]*)" button in the Unenroll dialog')
|
||||
def i_press_the_button_in_the_unenroll_dialog(step, value):
|
||||
button_css = 'section#unenroll-modal input[value="%s"]' % value
|
||||
world.browser.find_by_css(button_css).click()
|
||||
16
lms/djangoapps/portal/features/signup.feature
Normal file
16
lms/djangoapps/portal/features/signup.feature
Normal file
@@ -0,0 +1,16 @@
|
||||
Feature: Sign in
|
||||
In order to use the edX content
|
||||
As a new user
|
||||
I want to signup for a student account
|
||||
|
||||
Scenario: Sign up from the homepage
|
||||
Given I visit the homepage
|
||||
When I click the link with the text "Sign Up"
|
||||
And I fill in "email" on the registration form with "robot2@edx.org"
|
||||
And I fill in "password" on the registration form with "test"
|
||||
And I fill in "username" on the registration form with "robot2"
|
||||
And I fill in "name" on the registration form with "Robot Two"
|
||||
And I check the checkbox named "terms_of_service"
|
||||
And I check the checkbox named "honor_code"
|
||||
And I press the "Create My Account" button on the registration form
|
||||
Then I should see "THANKS FOR REGISTERING!" in the dashboard banner
|
||||
22
lms/djangoapps/portal/features/signup.py
Normal file
22
lms/djangoapps/portal/features/signup.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from lettuce import world, step
|
||||
|
||||
@step('I fill in "([^"]*)" on the registration form with "([^"]*)"$')
|
||||
def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value):
|
||||
register_form = world.browser.find_by_css('form#register_form')
|
||||
form_field = register_form.find_by_name(field)
|
||||
form_field.fill(value)
|
||||
|
||||
@step('I press the "([^"]*)" button on the registration form$')
|
||||
def i_press_the_button_on_the_registration_form(step, button):
|
||||
register_form = world.browser.find_by_css('form#register_form')
|
||||
register_form.find_by_value(button).click()
|
||||
|
||||
@step('I check the checkbox named "([^"]*)"$')
|
||||
def i_check_checkbox(step, checkbox):
|
||||
world.browser.find_by_name(checkbox).check()
|
||||
|
||||
@step('I should see "([^"]*)" in the dashboard banner$')
|
||||
def i_should_see_text_in_the_dashboard_banner_section(step, text):
|
||||
css_selector = "section.dashboard-banner h2"
|
||||
assert (text in world.browser.find_by_css(css_selector).text)
|
||||
|
||||
6
lms/djangoapps/terrain/__init__.py
Normal file
6
lms/djangoapps/terrain/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Use this as your lettuce terrain file so that the common steps
|
||||
# across all lms apps can be put in terrain/common
|
||||
# See https://groups.google.com/forum/?fromgroups=#!msg/lettuce-users/5VyU9B4HcX8/USgbGIJdS5QJ
|
||||
from terrain.browser import *
|
||||
from terrain.steps import *
|
||||
from terrain.factories import *
|
||||
26
lms/djangoapps/terrain/browser.py
Normal file
26
lms/djangoapps/terrain/browser.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from lettuce import before, after, world
|
||||
from splinter.browser import Browser
|
||||
from logging import getLogger
|
||||
import time
|
||||
|
||||
logger = getLogger(__name__)
|
||||
logger.info("Loading the lettuce acceptance testing terrain file...")
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
# Launch firefox
|
||||
world.browser = Browser('firefox')
|
||||
|
||||
@before.each_scenario
|
||||
def reset_data(scenario):
|
||||
# Clean out the django test database defined in the
|
||||
# envs/acceptance.py file: mitx_all/db/test_mitx.db
|
||||
logger.debug("Flushing the test database...")
|
||||
call_command('flush', interactive=False)
|
||||
|
||||
@after.all
|
||||
def teardown_browser(total):
|
||||
# Quit firefox
|
||||
world.browser.quit()
|
||||
34
lms/djangoapps/terrain/factories.py
Normal file
34
lms/djangoapps/terrain/factories.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import factory
|
||||
from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Test'
|
||||
level_of_education = None
|
||||
gender = 'm'
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot+test@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Test'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
171
lms/djangoapps/terrain/steps.py
Normal file
171
lms/djangoapps/terrain/steps.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from lettuce import world, step
|
||||
from factories import *
|
||||
from django.core.management import call_command
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from urllib import quote_plus
|
||||
from nose.tools import assert_equals
|
||||
from bs4 import BeautifulSoup
|
||||
import time
|
||||
import re
|
||||
import os.path
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@step(u'I wait (?:for )?"(\d+)" seconds?$')
|
||||
def wait(step, seconds):
|
||||
time.sleep(float(seconds))
|
||||
|
||||
@step('I (?:visit|access|open) the homepage$')
|
||||
def i_visit_the_homepage(step):
|
||||
world.browser.visit(django_url('/'))
|
||||
assert world.browser.is_element_present_by_css('header.global', 10)
|
||||
|
||||
@step(u'I (?:visit|access|open) the dashboard$')
|
||||
def i_visit_the_dashboard(step):
|
||||
world.browser.visit(django_url('/dashboard'))
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
|
||||
@step('I should be on the dashboard page$')
|
||||
def i_should_be_on_the_dashboard(step):
|
||||
assert world.browser.is_element_present_by_css('section.container.dashboard', 5)
|
||||
assert world.browser.title == 'Dashboard'
|
||||
|
||||
@step(u'I (?:visit|access|open) the courses page$')
|
||||
def i_am_on_the_courses_page(step):
|
||||
world.browser.visit(django_url('/courses'))
|
||||
assert world.browser.is_element_present_by_css('section.courses')
|
||||
|
||||
@step(u'I press the "([^"]*)" button$')
|
||||
def and_i_press_the_button(step, value):
|
||||
button_css = 'input[value="%s"]' % value
|
||||
world.browser.find_by_css(button_css).first.click()
|
||||
|
||||
@step('I should see that the path is "([^"]*)"$')
|
||||
def i_should_see_that_the_path_is(step, path):
|
||||
assert world.browser.url == django_url(path)
|
||||
|
||||
@step(u'the page title should be "([^"]*)"$')
|
||||
def the_page_title_should_be(step, title):
|
||||
assert_equals(world.browser.title, title)
|
||||
|
||||
@step('I am a logged in user$')
|
||||
def i_am_logged_in_user(step):
|
||||
create_user('robot')
|
||||
log_in('robot@edx.org','test')
|
||||
|
||||
@step('I am not logged in$')
|
||||
def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
|
||||
@step('I am registered for a course$')
|
||||
def i_am_registered_for_a_course(step):
|
||||
create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall')
|
||||
|
||||
@step('I am registered for course "([^"]*)"$')
|
||||
def i_am_registered_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id)
|
||||
|
||||
@step('I am staff for course "([^"]*)"$')
|
||||
def i_am_staff_for_course_by_id(step, course_id):
|
||||
register_by_course_id(course_id, True)
|
||||
|
||||
@step('I log in$')
|
||||
def i_log_in(step):
|
||||
log_in('robot@edx.org','test')
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
def i_am_an_edx_user(step):
|
||||
create_user('robot')
|
||||
|
||||
#### helper functions
|
||||
@world.absorb
|
||||
def create_user(uname):
|
||||
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
|
||||
portal_user.set_password('test')
|
||||
portal_user.save()
|
||||
|
||||
registration = RegistrationFactory(user=portal_user)
|
||||
registration.register(portal_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = UserProfileFactory(user=portal_user)
|
||||
|
||||
@world.absorb
|
||||
def log_in(email, password):
|
||||
world.browser.cookies.delete()
|
||||
world.browser.visit(django_url('/'))
|
||||
world.browser.is_element_present_by_css('header.global', 10)
|
||||
world.browser.click_link_by_href('#login-modal')
|
||||
login_form = world.browser.find_by_css('form#login_form')
|
||||
login_form.find_by_name('email').fill(email)
|
||||
login_form.find_by_name('password').fill(password)
|
||||
login_form.find_by_name('submit').click()
|
||||
|
||||
# wait for the page to redraw
|
||||
assert world.browser.is_element_present_by_css('.content-wrapper', 10)
|
||||
|
||||
@world.absorb
|
||||
def register_by_course_id(course_id, is_staff=False):
|
||||
create_user('robot')
|
||||
u = User.objects.get(username='robot')
|
||||
if is_staff:
|
||||
u.is_staff=True
|
||||
u.save()
|
||||
CourseEnrollment.objects.get_or_create(user=u, course_id=course_id)
|
||||
|
||||
@world.absorb
|
||||
def save_the_html(path='/tmp'):
|
||||
u = world.browser.url
|
||||
html = world.browser.html.encode('ascii', 'ignore')
|
||||
filename = '%s.html' % quote_plus(u)
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(html)
|
||||
f.close
|
||||
|
||||
@world.absorb
|
||||
def save_the_course_content(path='/tmp'):
|
||||
html = world.browser.html.encode('ascii', 'ignore')
|
||||
soup = BeautifulSoup(html)
|
||||
|
||||
# get rid of the header, we only want to compare the body
|
||||
soup.head.decompose()
|
||||
|
||||
# for now, remove the data-id attributes, because they are
|
||||
# causing mismatches between cms-master and master
|
||||
for item in soup.find_all(attrs={'data-id': re.compile('.*')}):
|
||||
del item['data-id']
|
||||
|
||||
# we also need to remove them from unrendered problems,
|
||||
# where they are contained in the text of divs instead of
|
||||
# in attributes of tags
|
||||
# Be careful of whether or not it was the last attribute
|
||||
# and needs a trailing space
|
||||
for item in soup.find_all(text=re.compile(' data-id=".*?" ')):
|
||||
s = unicode(item.string)
|
||||
item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s))
|
||||
|
||||
for item in soup.find_all(text=re.compile(' data-id=".*?"')):
|
||||
s = unicode(item.string)
|
||||
item.string.replace_with(re.sub(' data-id=".*?"', ' ', s))
|
||||
|
||||
# prettify the html so it will compare better, with
|
||||
# each HTML tag on its own line
|
||||
output = soup.prettify()
|
||||
|
||||
# use string slicing to grab everything after 'courseware/' in the URL
|
||||
u = world.browser.url
|
||||
section_url = u[u.find('courseware/')+11:]
|
||||
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
filename = '%s.html' % (quote_plus(section_url))
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(output)
|
||||
f.close
|
||||
41
lms/envs/acceptance.py
Normal file
41
lms/envs/acceptance.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
This config file extends the test environment configuration
|
||||
so that we can run the lettuce acceptance tests.
|
||||
"""
|
||||
from .test import *
|
||||
|
||||
# You need to start the server in debug mode,
|
||||
# otherwise the browser will not render the pages correctly
|
||||
DEBUG = True
|
||||
|
||||
# Show the courses that are in the data directory
|
||||
COURSES_ROOT = ENV_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'data_dir': DATA_DIR,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set this up so that rake lms[acceptance] and running the
|
||||
# harvest command both use the same (test) database
|
||||
# which they can flush without messing up your dev db
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
|
||||
}
|
||||
}
|
||||
|
||||
# Do not display the YouTube videos in the browser while running the
|
||||
# acceptance tests. This makes them faster and more reliable
|
||||
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
|
||||
|
||||
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('portal',) # dummy app covers the home page, login, registration, and course enrollment
|
||||
23
lms/envs/cms/acceptance.py
Normal file
23
lms/envs/cms/acceptance.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
This config file is a copy of dev environment without the Debug
|
||||
Toolbar. I it suitable to run against acceptance tests.
|
||||
|
||||
"""
|
||||
from .dev import *
|
||||
|
||||
# REMOVE DEBUG TOOLBAR
|
||||
|
||||
INSTALLED_APPS = tuple(e for e in INSTALLED_APPS if e != 'debug_toolbar')
|
||||
INSTALLED_APPS = tuple(e for e in INSTALLED_APPS if e != 'debug_toolbar_mongo')
|
||||
|
||||
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
|
||||
if e != 'debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
|
||||
|
||||
########################### LETTUCE TESTING ##########################
|
||||
MITX_FEATURES['DISPLAY_TOY_COURSES'] = True
|
||||
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
# INSTALLED_APPS += ('portal',)
|
||||
|
||||
LETTUCE_APPS = ('portal',) # dummy app covers the home page, login, registration, and course enrollment
|
||||
@@ -76,6 +76,8 @@ MITX_FEATURES = {
|
||||
|
||||
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
|
||||
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
|
||||
# extrernal access methods
|
||||
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
|
||||
'AUTH_USE_OPENID': False,
|
||||
@@ -407,6 +409,7 @@ courseware_only_js += [
|
||||
]
|
||||
|
||||
main_vendor_js = [
|
||||
'js/vendor/RequireJS.js',
|
||||
'js/vendor/json2.js',
|
||||
'js/vendor/jquery.min.js',
|
||||
'js/vendor/jquery-ui.min.js',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"js_files": [
|
||||
"/static/js/vendor/RequireJS.js",
|
||||
"/static/js/vendor/jquery.min.js",
|
||||
"/static/js/vendor/jquery-ui.min.js",
|
||||
"/static/js/vendor/jquery.leanModal.min.js",
|
||||
|
||||
89
lms/static/coffee/spec/requirejs_spec.coffee
Normal file
89
lms/static/coffee/spec/requirejs_spec.coffee
Normal file
@@ -0,0 +1,89 @@
|
||||
describe "RequireJS namespacing", ->
|
||||
beforeEach ->
|
||||
|
||||
# Jasmine does not provide a way to use the typeof operator. We need
|
||||
# to create our own custom matchers so that a TypeError is not thrown.
|
||||
@addMatchers
|
||||
requirejsTobeUndefined: ->
|
||||
typeof requirejs is "undefined"
|
||||
|
||||
requireTobeUndefined: ->
|
||||
typeof require is "undefined"
|
||||
|
||||
defineTobeUndefined: ->
|
||||
typeof define is "undefined"
|
||||
|
||||
|
||||
it "check that the RequireJS object is present in the global namespace", ->
|
||||
expect(RequireJS).toEqual jasmine.any(Object)
|
||||
expect(window.RequireJS).toEqual jasmine.any(Object)
|
||||
|
||||
it "check that requirejs(), require(), and define() are not in the global namespace", ->
|
||||
|
||||
# The custom matchers that we defined in the beforeEach() function do
|
||||
# not operate on an object. We pass a dummy empty object {} not to
|
||||
# confuse Jasmine.
|
||||
expect({}).requirejsTobeUndefined()
|
||||
expect({}).requireTobeUndefined()
|
||||
expect({}).defineTobeUndefined()
|
||||
expect(window.requirejs).not.toBeDefined()
|
||||
expect(window.require).not.toBeDefined()
|
||||
expect(window.define).not.toBeDefined()
|
||||
|
||||
|
||||
describe "RequireJS module creation", ->
|
||||
inDefineCallback = undefined
|
||||
inRequireCallback = undefined
|
||||
it "check that we can use RequireJS to define() and require() a module", ->
|
||||
|
||||
# Because Require JS works asynchronously when defining and requiring
|
||||
# modules, we need to use the special Jasmine functions runs(), and
|
||||
# waitsFor() to set up this test.
|
||||
runs ->
|
||||
|
||||
# Initialize the variable that we will test for. They will be set
|
||||
# to true in the appropriate callback functions called by Require
|
||||
# JS. If their values do not change, this will mean that something
|
||||
# is not working as is intended.
|
||||
inDefineCallback = false
|
||||
inRequireCallback = false
|
||||
|
||||
# Define our test module.
|
||||
RequireJS.define "test_module", [], ->
|
||||
inDefineCallback = true
|
||||
|
||||
# This module returns an object. It can be accessed via the
|
||||
# Require JS require() function.
|
||||
module_status: "OK"
|
||||
|
||||
|
||||
# Require our defined test module.
|
||||
RequireJS.require ["test_module"], (test_module) ->
|
||||
inRequireCallback = true
|
||||
|
||||
# If our test module was defined properly, then we should
|
||||
# be able to get the object it returned, and query some
|
||||
# property.
|
||||
expect(test_module.module_status).toBe "OK"
|
||||
|
||||
|
||||
|
||||
# We will wait for a specified amount of time (1 second), before
|
||||
# checking if our module was defined and that we were able to
|
||||
# require() the module.
|
||||
waitsFor (->
|
||||
|
||||
# If at least one of the callback functions was not reached, we
|
||||
# fail this test.
|
||||
return false if (inDefineCallback isnt true) or (inRequireCallback isnt true)
|
||||
|
||||
# Both of the callbacks were reached.
|
||||
true
|
||||
), "We should eventually end up in the defined callback", 1000
|
||||
|
||||
# The final test behavior, after waitsFor() finishes waiting.
|
||||
runs ->
|
||||
expect(inDefineCallback).toBeTruthy()
|
||||
expect(inRequireCallback).toBeTruthy()
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
<div id="video_${id}" class="video" data-streams="${streams}"
|
||||
data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}"
|
||||
data-start="${start}" data-end="${end}">
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
<div id="stub_out_video_for_testing"></div>
|
||||
%else:
|
||||
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}">
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
% if source:
|
||||
<div class="video-sources">
|
||||
<p>Download video <a href="${source}">here</a>.</p>
|
||||
|
||||
@@ -3,3 +3,7 @@ coverage
|
||||
nosexcover
|
||||
pylint
|
||||
pep8
|
||||
lettuce
|
||||
selenium
|
||||
factory_boy
|
||||
splinter
|
||||
Reference in New Issue
Block a user