+ """.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)
diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html
index 65fc7fb9bb..c42ad73faf 100644
--- a/common/lib/capa/capa/templates/openendedinput.html
+++ b/common/lib/capa/capa/templates/openendedinput.html
@@ -27,6 +27,30 @@
% endif
- ${msg|n}
+ ${msg|n}
+ % if status in ['correct','incorrect']:
+
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 4c10a1703a..d65fa1f40a 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -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:
diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss
index b25ab3d3a2..929b6dcb48 100644
--- a/common/lib/xmodule/xmodule/css/capa/display.scss
+++ b/common/lib/xmodule/xmodule/css/capa/display.scss
@@ -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;
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 1c0ace9e59..ba746fecb8 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -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) =>
diff --git a/common/static/js/vendor/RequireJS.js b/common/static/js/vendor/RequireJS.js
new file mode 100644
index 0000000000..a0526930ef
--- /dev/null
+++ b/common/static/js/vendor/RequireJS.js
@@ -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;dthis.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;0f.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()
diff --git a/doc/testing.md b/doc/testing.md
index ee54ae74d9..694a9e8231 100644
--- a/doc/testing.md
+++ b/doc/testing.md
@@ -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.
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py
new file mode 100644
index 0000000000..aecaa139ff
--- /dev/null
+++ b/lms/djangoapps/courseware/features/courses.py
@@ -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
diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature
new file mode 100644
index 0000000000..21c7e84541
--- /dev/null
+++ b/lms/djangoapps/courseware/features/courseware.feature
@@ -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
diff --git a/lms/djangoapps/courseware/features/courseware.py b/lms/djangoapps/courseware/features/courseware.py
new file mode 100644
index 0000000000..05ecd63f4b
--- /dev/null
+++ b/lms/djangoapps/courseware/features/courseware.py
@@ -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)
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py
new file mode 100644
index 0000000000..8850c88fef
--- /dev/null
+++ b/lms/djangoapps/courseware/features/courseware_common.py
@@ -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
diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature
new file mode 100644
index 0000000000..2e9c4f1886
--- /dev/null
+++ b/lms/djangoapps/courseware/features/high-level-tabs.feature
@@ -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"
diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature
new file mode 100644
index 0000000000..3c7043ba54
--- /dev/null
+++ b/lms/djangoapps/courseware/features/openended.feature
@@ -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
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/features/openended.py b/lms/djangoapps/courseware/features/openended.py
new file mode 100644
index 0000000000..d37f9a0fae
--- /dev/null
+++ b/lms/djangoapps/courseware/features/openended.py
@@ -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)
diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature
new file mode 100644
index 0000000000..90d097144a
--- /dev/null
+++ b/lms/djangoapps/courseware/features/smart-accordion.feature
@@ -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
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py
new file mode 100644
index 0000000000..95d3396f57
--- /dev/null
+++ b/lms/djangoapps/courseware/features/smart-accordion.py
@@ -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
diff --git a/lms/djangoapps/portal/README.md b/lms/djangoapps/portal/README.md
new file mode 100644
index 0000000000..09930ec8fb
--- /dev/null
+++ b/lms/djangoapps/portal/README.md
@@ -0,0 +1,15 @@
+## acceptance_testing
+
+This fake django app is here to support acceptance testing using lettuce +
+splinter (which wraps selenium).
+
+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 this link.
diff --git a/lms/djangoapps/portal/__init__.py b/lms/djangoapps/portal/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/djangoapps/portal/features/common.py b/lms/djangoapps/portal/features/common.py
new file mode 100644
index 0000000000..20c2ab56b8
--- /dev/null
+++ b/lms/djangoapps/portal/features/common.py
@@ -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)
diff --git a/lms/djangoapps/portal/features/factories.py b/lms/djangoapps/portal/features/factories.py
new file mode 100644
index 0000000000..07b615f468
--- /dev/null
+++ b/lms/djangoapps/portal/features/factories.py
@@ -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)
diff --git a/lms/djangoapps/portal/features/homepage.feature b/lms/djangoapps/portal/features/homepage.feature
new file mode 100644
index 0000000000..06a45c4bfa
--- /dev/null
+++ b/lms/djangoapps/portal/features/homepage.feature
@@ -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 ""
+ When I click the link with the text ""
+ Then I should see that the path is ""
+
+ 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 "" 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
diff --git a/lms/djangoapps/portal/features/homepage.py b/lms/djangoapps/portal/features/homepage.py
new file mode 100644
index 0000000000..638d65077c
--- /dev/null
+++ b/lms/djangoapps/portal/features/homepage.py
@@ -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)
diff --git a/lms/djangoapps/portal/features/login.feature b/lms/djangoapps/portal/features/login.feature
new file mode 100644
index 0000000000..23317b4876
--- /dev/null
+++ b/lms/djangoapps/portal/features/login.feature
@@ -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 "/"
diff --git a/lms/djangoapps/portal/features/login.py b/lms/djangoapps/portal/features/login.py
new file mode 100644
index 0000000000..5f200eb259
--- /dev/null
+++ b/lms/djangoapps/portal/features/login.py
@@ -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)
diff --git a/lms/djangoapps/portal/features/registration.feature b/lms/djangoapps/portal/features/registration.feature
new file mode 100644
index 0000000000..d8a6796ee3
--- /dev/null
+++ b/lms/djangoapps/portal/features/registration.feature
@@ -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
\ No newline at end of file
diff --git a/lms/djangoapps/portal/features/registration.py b/lms/djangoapps/portal/features/registration.py
new file mode 100644
index 0000000000..124bed4923
--- /dev/null
+++ b/lms/djangoapps/portal/features/registration.py
@@ -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()
diff --git a/lms/djangoapps/portal/features/signup.feature b/lms/djangoapps/portal/features/signup.feature
new file mode 100644
index 0000000000..b28a6819a1
--- /dev/null
+++ b/lms/djangoapps/portal/features/signup.feature
@@ -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
diff --git a/lms/djangoapps/portal/features/signup.py b/lms/djangoapps/portal/features/signup.py
new file mode 100644
index 0000000000..afde72b589
--- /dev/null
+++ b/lms/djangoapps/portal/features/signup.py
@@ -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)
+
\ No newline at end of file
diff --git a/lms/djangoapps/terrain/__init__.py b/lms/djangoapps/terrain/__init__.py
new file mode 100644
index 0000000000..dd6869e7fd
--- /dev/null
+++ b/lms/djangoapps/terrain/__init__.py
@@ -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 *
\ No newline at end of file
diff --git a/lms/djangoapps/terrain/browser.py b/lms/djangoapps/terrain/browser.py
new file mode 100644
index 0000000000..7fe684e153
--- /dev/null
+++ b/lms/djangoapps/terrain/browser.py
@@ -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()
diff --git a/lms/djangoapps/terrain/factories.py b/lms/djangoapps/terrain/factories.py
new file mode 100644
index 0000000000..ddab9e2b06
--- /dev/null
+++ b/lms/djangoapps/terrain/factories.py
@@ -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)
diff --git a/lms/djangoapps/terrain/steps.py b/lms/djangoapps/terrain/steps.py
new file mode 100644
index 0000000000..ce82a0a044
--- /dev/null
+++ b/lms/djangoapps/terrain/steps.py
@@ -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
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
new file mode 100644
index 0000000000..e0857a4392
--- /dev/null
+++ b/lms/envs/acceptance.py
@@ -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
diff --git a/lms/envs/cms/acceptance.py b/lms/envs/cms/acceptance.py
new file mode 100644
index 0000000000..e5ee2937f4
--- /dev/null
+++ b/lms/envs/cms/acceptance.py
@@ -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
diff --git a/lms/envs/common.py b/lms/envs/common.py
index d18c82b754..a24422df50 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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',
diff --git a/lms/static/coffee/files.json b/lms/static/coffee/files.json
index 4721ef58bb..5dc03613b9 100644
--- a/lms/static/coffee/files.json
+++ b/lms/static/coffee/files.json
@@ -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",
diff --git a/lms/static/coffee/spec/requirejs_spec.coffee b/lms/static/coffee/spec/requirejs_spec.coffee
new file mode 100644
index 0000000000..10d34a2f75
--- /dev/null
+++ b/lms/static/coffee/spec/requirejs_spec.coffee
@@ -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()
+
+
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 6e45a91c31..4d4df8c3c7 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -2,19 +2,21 @@