From 20b04c9bf20bdd9be4e57ce7a1b9e0033b2ea325 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 13:06:15 -0500 Subject: [PATCH 01/42] Fixed broken calculator test in lms coffee scripts --- lms/static/coffee/spec/calculator_spec.coffee | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee index 072d220a44..a8210ce68b 100644 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ b/lms/static/coffee/spec/calculator_spec.coffee @@ -31,12 +31,19 @@ describe 'Calculator', -> $('form#calculator').submit() describe 'toggle', -> - it 'toggle the calculator and focus the input', -> - spyOn $.fn, 'focus' - @calculator.toggle(jQuery.Event("click")) + it 'focuses the input when toggled', -> - expect($('li.calc-main')).toHaveClass('open') - expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() + # Since the focus is called asynchronously, we need to + # wait until focus() is called. + didFocus = false + runs -> + spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true + @calculator.toggle(jQuery.Event("click")) + + waitsFor (-> didFocus), "focus() should have been called on the input", 1000 + + runs -> + expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() it 'toggle the close button on the calculator button', -> @calculator.toggle(jQuery.Event("click")) From 618de5df25642c4962b5870438aa13586b2ffd36 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 17:08:12 -0500 Subject: [PATCH 02/42] Fixed import error for histogram.coffee --- common/templates/jasmine/base.html | 7 ++++--- lms/static/coffee/files.json | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html index 96507bdebf..40308c7bbe 100644 --- a/common/templates/jasmine/base.html +++ b/common/templates/jasmine/base.html @@ -13,14 +13,15 @@ + {% load compressed %} + {# static files #} + {% compressed_js 'js-test-source' %} + {# source files #} {% for url in suite.js_files %} {% endfor %} - {% load compressed %} - {# static files #} - {% compressed_js 'js-test-source' %} {# spec files #} {% compressed_js 'spec' %} diff --git a/lms/static/coffee/files.json b/lms/static/coffee/files.json index 5dc03613b9..0efe488dd9 100644 --- a/lms/static/coffee/files.json +++ b/lms/static/coffee/files.json @@ -5,8 +5,5 @@ "/static/js/vendor/jquery-ui.min.js", "/static/js/vendor/jquery.leanModal.min.js", "/static/js/vendor/flot/jquery.flot.js" - ], - "static_files": [ - "js/application.js" ] } From 6b94090e673ab80723de67d3d95be819714ede44 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 6 Mar 2013 17:41:44 -0500 Subject: [PATCH 03/42] Fixed jasmine test issue with cms caused by previous change to order of file import. Introduced static_files list in files.json (supported in original django_jasmine implementation) that loads static files before any compiled files. --- cms/static/coffee/files.json | 18 +++++++++--------- common/templates/jasmine/base.html | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index 2249813b04..e7a66b5bc0 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -1,12 +1,12 @@ { - "js_files": [ - "/static/js/vendor/RequireJS.js", - "/static/js/vendor/jquery.min.js", - "/static/js/vendor/jquery-ui.min.js", - "/static/js/vendor/jquery.ui.draggable.js", - "/static/js/vendor/jquery.cookie.js", - "/static/js/vendor/json2.js", - "/static/js/vendor/underscore-min.js", - "/static/js/vendor/backbone-min.js" + "static_files": [ + "js/vendor/RequireJS.js", + "js/vendor/jquery.min.js", + "js/vendor/jquery-ui.min.js", + "js/vendor/jquery.ui.draggable.js", + "js/vendor/jquery.cookie.js", + "js/vendor/json2.js", + "js/vendor/underscore-min.js", + "js/vendor/backbone-min.js" ] } diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html index 40308c7bbe..9a1b3bed92 100644 --- a/common/templates/jasmine/base.html +++ b/common/templates/jasmine/base.html @@ -15,6 +15,10 @@ {% load compressed %} {# static files #} + {% for url in suite.static_files %} + + {% endfor %} + {% compressed_js 'js-test-source' %} {# source files #} From a777a9e6e3673d9c049e2c3ad7cba6d7ec756c62 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 11 Mar 2013 11:20:20 -0400 Subject: [PATCH 04/42] Updated broken tabs.js Jasmine tests. --- lms/static/coffee/spec/modules/tab_spec.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lms/static/coffee/spec/modules/tab_spec.coffee b/lms/static/coffee/spec/modules/tab_spec.coffee index 909f0d7cda..6fba470974 100644 --- a/lms/static/coffee/spec/modules/tab_spec.coffee +++ b/lms/static/coffee/spec/modules/tab_spec.coffee @@ -22,18 +22,23 @@ describe 'Tab', -> it 'bind the tabs', -> expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow + # As of jQuery 1.9, the onShow callback is deprecated + # http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate + # The code below tests that onShow does what is expected, + # but note that onShow will NOT be called when the user + # clicks on the tab if we're using jQuery version >= 1.9 describe 'onShow', -> beforeEach -> @tab = new Tab 1, @items - $('[href="#tab-1-0"]').click() + @tab.onShow($('#tab-1-0'), {'index': 1}) it 'replace content in the container', -> - $('[href="#tab-1-1"]').click() + @tab.onShow($('#tab-1-1'), {'index': 1}) expect($('#tab-1-0').html()).toEqual '' expect($('#tab-1-1').html()).toEqual 'Video 2' expect($('#tab-1-2').html()).toEqual '' it 'trigger contentChanged event on the element', -> spyOnEvent @tab.el, 'contentChanged' - $('[href="#tab-1-1"]').click() + @tab.onShow($('#tab-1-1'), {'index': 1}) expect('contentChanged').toHaveBeenTriggeredOn @tab.el From 3568db73e39ed31611ebfad4d02e8673723d48aa Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 11 Mar 2013 13:54:44 -0400 Subject: [PATCH 05/42] initial wiring of 404/500 error pages for Studio --- cms/djangoapps/contentstore/views.py | 10 +++++++++- cms/urls.py | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 34003d71a4..345861f979 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -18,7 +18,7 @@ from django.core.files.temp import NamedTemporaryFile # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' from PIL import Image -from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError, HttpResponseNotFound from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.core.context_processors import csrf @@ -1569,3 +1569,11 @@ def event(request): console logs don't get distracted :-) ''' return HttpResponse(True) + + +def render_404(request): + return HttpResponseNotFound(render_to_string('404.html', {})) + + +def render_500(request): + return HttpResponseServerError(render_to_string('500.html', {})) diff --git a/cms/urls.py b/cms/urls.py index d43b9bc44c..69ce4a540d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -104,3 +104,9 @@ if settings.ENABLE_JASMINE: urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns = patterns(*urlpatterns) + +#Custom error pages +handler404 = 'contentstore.views.render_404' +handler500 = 'contentstore.views.render_500' + + From 9d8c023acb05271a71e040236df5db7bbddb530b Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 11 Mar 2013 15:58:31 -0400 Subject: [PATCH 06/42] Removed calls to Navigation.bind() and Calculator.bind() that were causing phantomjs interpreter to fail with undefined method error. --- lms/static/coffee/spec/calculator_spec.coffee | 3 --- lms/static/coffee/spec/navigation_spec.coffee | 3 --- 2 files changed, 6 deletions(-) diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee index a8210ce68b..8258d8965a 100644 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ b/lms/static/coffee/spec/calculator_spec.coffee @@ -4,9 +4,6 @@ describe 'Calculator', -> @calculator = new Calculator describe 'bind', -> - beforeEach -> - Calculator.bind() - it 'bind the calculator button', -> expect($('.calc')).toHandleWith 'click', @calculator.toggle diff --git a/lms/static/coffee/spec/navigation_spec.coffee b/lms/static/coffee/spec/navigation_spec.coffee index 1340984e52..b351164b63 100644 --- a/lms/static/coffee/spec/navigation_spec.coffee +++ b/lms/static/coffee/spec/navigation_spec.coffee @@ -32,11 +32,9 @@ describe 'Navigation', -> heightStyle: 'content' it 'binds the accordionchange event', -> - Navigation.bind() expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log it 'bind the navigation toggle', -> - Navigation.bind() expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle describe 'when the #accordion does not exists', -> @@ -45,7 +43,6 @@ describe 'Navigation', -> it 'does not activate the accordion', -> spyOn $.fn, 'accordion' - Navigation.bind() expect($('#accordion').accordion).wasNotCalled() describe 'toggle', -> From 470e40272345c330cd11ae9049a84804659a1986 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 Mar 2013 10:51:34 -0400 Subject: [PATCH 07/42] patch for backward compatibility --- lms/djangoapps/django_comment_client/base/views.py | 3 +++ lms/djangoapps/django_comment_client/forum/views.py | 10 ++++++++++ lms/templates/discussion/_underscore_templates.html | 12 +++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 6734625a76..c021458ae3 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -116,6 +116,9 @@ def create_thread(request, course_id, commentable_id): thread.save() + #patch for backward compatibility with comments service + thread['pinned'] = False + if post.get('auto_subscribe', 'false').lower() == 'true': user = cc.User.from_django_user(request.user) user.follow(thread) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 50224e7de6..bb59a5675a 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -98,6 +98,9 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG else: thread['group_name'] = "" thread['group_string'] = "This post visible to everyone." + + #temporary patch for backward compatibility to comments service + thread['pinning'] = False query_params['page'] = page query_params['num_pages'] = num_pages @@ -245,6 +248,10 @@ def single_thread(request, course_id, discussion_id, thread_id): try: thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) + + #temporary patch for backward compatibility with comments service + thread["pinned"] = False + except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: log.error("Error loading single thread.") raise Http404 @@ -285,6 +292,9 @@ def single_thread(request, course_id, discussion_id, thread_id): if thread.get('group_id') and not thread.get('group_name'): thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name + #temporary patch for backward compatibility with comments service + thread["pinned"] = False + threads = [utils.safe_content(thread) for thread in threads] #recent_active_threads = cc.search_recent_active_threads( diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 5fdfb8aa82..21a881908f 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -45,11 +45,17 @@
${'<%- body %>'}
- - % if course and has_permission(user, 'openclose_thread', course.id): + + % if course and has_permission(user, 'openclose_thread', course.id) and False:
Pin Thread
- %else: + + + %elif False: ${"<% if (pinned) { %>"}
Pin Thread
From 0a53ec3ee6edb4f499a0a9073e9117bf13152123 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 Mar 2013 11:37:58 -0400 Subject: [PATCH 08/42] updates --- lms/templates/discussion/_underscore_templates.html | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 21a881908f..24e3b467be 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -45,17 +45,11 @@
${'<%- body %>'}
- - % if course and has_permission(user, 'openclose_thread', course.id) and False: + % if course and has_permission(user, 'openclose_thread', course.id):
Pin Thread
- - %elif False: + %else: ${"<% if (pinned) { %>"}
Pin Thread
@@ -63,9 +57,6 @@ % endif - - - ${'<% if (obj.courseware_url) { %>'}
(this post is about ${'<%- courseware_title %>'}) From a761e2e8297518ed03eab69b85a5f17735988701 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 Mar 2013 12:38:28 -0400 Subject: [PATCH 09/42] make pinning backward compatible --- .../views/discussion_thread_show_view.coffee | 4 +- .../django_comment_client/base/views.py | 5 +- .../django_comment_client/forum/views.py | 16 ++- lms/lib/comment_client/models.py | 131 ------------------ 4 files changed, 16 insertions(+), 140 deletions(-) delete mode 100644 lms/lib/comment_client/models.py diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index a5a1deac10..56525af347 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -128,7 +128,9 @@ if Backbone? type: "POST" success: (response, textStatus) => if textStatus == 'success' - @model.set('pinned', true) + @model.set('pinned', true) + error: => + $('.admin-pin').text("Pinning not currently available") unPin: -> url = @model.urlFor("unPinThread") diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index c021458ae3..6cb45ee64d 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -116,8 +116,9 @@ def create_thread(request, course_id, commentable_id): thread.save() - #patch for backward compatibility with comments service - thread['pinned'] = False + #patch for backward compatibility to comments service + if not 'pinned' in thread.attributes: + thread['pinned'] = False if post.get('auto_subscribe', 'false').lower() == 'true': user = cc.User.from_django_user(request.user) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index bb59a5675a..445a9b139e 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -99,8 +99,10 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG thread['group_name'] = "" thread['group_string'] = "This post visible to everyone." - #temporary patch for backward compatibility to comments service - thread['pinning'] = False + #patch for backward compatibility to comments service + if not 'pinned' in thread: + thread['pinned'] = False + query_params['page'] = page query_params['num_pages'] = num_pages @@ -249,8 +251,9 @@ def single_thread(request, course_id, discussion_id, thread_id): try: thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) - #temporary patch for backward compatibility with comments service - thread["pinned"] = False + #patch for backward compatibility with comments service + if not 'pinned' in thread.attributes: + thread['pinned'] = False except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: log.error("Error loading single thread.") @@ -292,8 +295,9 @@ def single_thread(request, course_id, discussion_id, thread_id): if thread.get('group_id') and not thread.get('group_name'): thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name - #temporary patch for backward compatibility with comments service - thread["pinned"] = False + #patch for backward compatibility with comments service + if not "pinned" in thread: + thread["pinned"] = False threads = [utils.safe_content(thread) for thread in threads] diff --git a/lms/lib/comment_client/models.py b/lms/lib/comment_client/models.py deleted file mode 100644 index 2a8992554d..0000000000 --- a/lms/lib/comment_client/models.py +++ /dev/null @@ -1,131 +0,0 @@ -from utils import * - - -class Model(object): - - accessible_fields = ['id'] - updatable_fields = ['id'] - initializable_fields = ['id'] - base_url = None - default_retrieve_params = {} - - DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete'] - DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post'] - DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID - - def __init__(self, *args, **kwargs): - self.attributes = extract(kwargs, self.accessible_fields) - self.retrieved = False - - def __getattr__(self, name): - if name == 'id': - return self.attributes.get('id', None) - try: - return self.attributes[name] - except KeyError: - if self.retrieved or self.id is None: - raise AttributeError("Field {0} does not exist".format(name)) - self.retrieve() - return self.__getattr__(name) - - def __setattr__(self, name, value): - if name == 'attributes' or name not in self.accessible_fields: - super(Model, self).__setattr__(name, value) - else: - self.attributes[name] = value - - def __getitem__(self, key): - if key not in self.accessible_fields: - raise KeyError("Field {0} does not exist".format(key)) - return self.attributes.get(key) - - def __setitem__(self, key, value): - if key not in self.accessible_fields: - raise KeyError("Field {0} does not exist".format(key)) - self.attributes.__setitem__(key, value) - - def items(self, *args, **kwargs): - return self.attributes.items(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.attributes.get(*args, **kwargs) - - def to_dict(self): - self.retrieve() - return self.attributes - - def retrieve(self, *args, **kwargs): - if not self.retrieved: - self._retrieve(*args, **kwargs) - self.retrieved = True - return self - - def _retrieve(self, *args, **kwargs): - url = self.url(action='get', params=self.attributes) - response = perform_request('get', url, self.default_retrieve_params) - self.update_attributes(**response) - - @classmethod - def find(cls, id): - return cls(id=id) - - def update_attributes(self, *args, **kwargs): - for k, v in kwargs.items(): - if k in self.accessible_fields: - self.__setattr__(k, v) - else: - raise AttributeError("Field {0} does not exist".format(k)) - - def updatable_attributes(self): - return extract(self.attributes, self.updatable_fields) - - def initializable_attributes(self): - return extract(self.attributes, self.initializable_fields) - - @classmethod - def before_save(cls, instance): - pass - - @classmethod - def after_save(cls, instance): - pass - - def save(self): - self.__class__.before_save(self) - if self.id: # if we have id already, treat this as an update - url = self.url(action='put', params=self.attributes) - response = perform_request('put', url, self.updatable_attributes()) - else: # otherwise, treat this as an insert - url = self.url(action='post', params=self.attributes) - response = perform_request('post', url, self.initializable_attributes()) - self.retrieved = True - self.update_attributes(**response) - self.__class__.after_save(self) - - def delete(self): - url = self.url(action='delete', params=self.attributes) - response = perform_request('delete', url) - self.retrieved = True - self.update_attributes(**response) - - @classmethod - def url_with_id(cls, params={}): - return cls.base_url + '/' + str(params['id']) - - @classmethod - def url_without_id(cls, params={}): - return cls.base_url - - @classmethod - def url(cls, action, params={}): - if cls.base_url is None: - raise CommentClientError("Must provide base_url when using default url function") - if action not in cls.DEFAULT_ACTIONS: - raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS))) - elif action in cls.DEFAULT_ACTIONS_WITH_ID: - try: - return cls.url_with_id(params) - except KeyError: - raise CommentClientError("Cannot perform action {0} without id".format(action)) - else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now - return cls.url_without_id() From 68cb63daa81212676bf87469d8e36eed090b8036 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 Mar 2013 12:42:59 -0400 Subject: [PATCH 10/42] accidentally deleted models file --- common/models.py | 131 +++++++++++++++++++++++++++++++ lms/lib/comment_client/models.py | 131 +++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 common/models.py create mode 100644 lms/lib/comment_client/models.py diff --git a/common/models.py b/common/models.py new file mode 100644 index 0000000000..2a8992554d --- /dev/null +++ b/common/models.py @@ -0,0 +1,131 @@ +from utils import * + + +class Model(object): + + accessible_fields = ['id'] + updatable_fields = ['id'] + initializable_fields = ['id'] + base_url = None + default_retrieve_params = {} + + DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete'] + DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post'] + DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID + + def __init__(self, *args, **kwargs): + self.attributes = extract(kwargs, self.accessible_fields) + self.retrieved = False + + def __getattr__(self, name): + if name == 'id': + return self.attributes.get('id', None) + try: + return self.attributes[name] + except KeyError: + if self.retrieved or self.id is None: + raise AttributeError("Field {0} does not exist".format(name)) + self.retrieve() + return self.__getattr__(name) + + def __setattr__(self, name, value): + if name == 'attributes' or name not in self.accessible_fields: + super(Model, self).__setattr__(name, value) + else: + self.attributes[name] = value + + def __getitem__(self, key): + if key not in self.accessible_fields: + raise KeyError("Field {0} does not exist".format(key)) + return self.attributes.get(key) + + def __setitem__(self, key, value): + if key not in self.accessible_fields: + raise KeyError("Field {0} does not exist".format(key)) + self.attributes.__setitem__(key, value) + + def items(self, *args, **kwargs): + return self.attributes.items(*args, **kwargs) + + def get(self, *args, **kwargs): + return self.attributes.get(*args, **kwargs) + + def to_dict(self): + self.retrieve() + return self.attributes + + def retrieve(self, *args, **kwargs): + if not self.retrieved: + self._retrieve(*args, **kwargs) + self.retrieved = True + return self + + def _retrieve(self, *args, **kwargs): + url = self.url(action='get', params=self.attributes) + response = perform_request('get', url, self.default_retrieve_params) + self.update_attributes(**response) + + @classmethod + def find(cls, id): + return cls(id=id) + + def update_attributes(self, *args, **kwargs): + for k, v in kwargs.items(): + if k in self.accessible_fields: + self.__setattr__(k, v) + else: + raise AttributeError("Field {0} does not exist".format(k)) + + def updatable_attributes(self): + return extract(self.attributes, self.updatable_fields) + + def initializable_attributes(self): + return extract(self.attributes, self.initializable_fields) + + @classmethod + def before_save(cls, instance): + pass + + @classmethod + def after_save(cls, instance): + pass + + def save(self): + self.__class__.before_save(self) + if self.id: # if we have id already, treat this as an update + url = self.url(action='put', params=self.attributes) + response = perform_request('put', url, self.updatable_attributes()) + else: # otherwise, treat this as an insert + url = self.url(action='post', params=self.attributes) + response = perform_request('post', url, self.initializable_attributes()) + self.retrieved = True + self.update_attributes(**response) + self.__class__.after_save(self) + + def delete(self): + url = self.url(action='delete', params=self.attributes) + response = perform_request('delete', url) + self.retrieved = True + self.update_attributes(**response) + + @classmethod + def url_with_id(cls, params={}): + return cls.base_url + '/' + str(params['id']) + + @classmethod + def url_without_id(cls, params={}): + return cls.base_url + + @classmethod + def url(cls, action, params={}): + if cls.base_url is None: + raise CommentClientError("Must provide base_url when using default url function") + if action not in cls.DEFAULT_ACTIONS: + raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS))) + elif action in cls.DEFAULT_ACTIONS_WITH_ID: + try: + return cls.url_with_id(params) + except KeyError: + raise CommentClientError("Cannot perform action {0} without id".format(action)) + else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now + return cls.url_without_id() diff --git a/lms/lib/comment_client/models.py b/lms/lib/comment_client/models.py new file mode 100644 index 0000000000..bf5f576a44 --- /dev/null +++ b/lms/lib/comment_client/models.py @@ -0,0 +1,131 @@ +from .utils import * + + +class Model(object): + + accessible_fields = ['id'] + updatable_fields = ['id'] + initializable_fields = ['id'] + base_url = None + default_retrieve_params = {} + + DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete'] + DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post'] + DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID + + def __init__(self, *args, **kwargs): + self.attributes = extract(kwargs, self.accessible_fields) + self.retrieved = False + + def __getattr__(self, name): + if name == 'id': + return self.attributes.get('id', None) + try: + return self.attributes[name] + except KeyError: + if self.retrieved or self.id is None: + raise AttributeError("Field {0} does not exist".format(name)) + self.retrieve() + return self.__getattr__(name) + + def __setattr__(self, name, value): + if name == 'attributes' or name not in self.accessible_fields: + super(Model, self).__setattr__(name, value) + else: + self.attributes[name] = value + + def __getitem__(self, key): + if key not in self.accessible_fields: + raise KeyError("Field {0} does not exist".format(key)) + return self.attributes.get(key) + + def __setitem__(self, key, value): + if key not in self.accessible_fields: + raise KeyError("Field {0} does not exist".format(key)) + self.attributes.__setitem__(key, value) + + def items(self, *args, **kwargs): + return self.attributes.items(*args, **kwargs) + + def get(self, *args, **kwargs): + return self.attributes.get(*args, **kwargs) + + def to_dict(self): + self.retrieve() + return self.attributes + + def retrieve(self, *args, **kwargs): + if not self.retrieved: + self._retrieve(*args, **kwargs) + self.retrieved = True + return self + + def _retrieve(self, *args, **kwargs): + url = self.url(action='get', params=self.attributes) + response = perform_request('get', url, self.default_retrieve_params) + self.update_attributes(**response) + + @classmethod + def find(cls, id): + return cls(id=id) + + def update_attributes(self, *args, **kwargs): + for k, v in kwargs.items(): + if k in self.accessible_fields: + self.__setattr__(k, v) + else: + raise AttributeError("Field {0} does not exist".format(k)) + + def updatable_attributes(self): + return extract(self.attributes, self.updatable_fields) + + def initializable_attributes(self): + return extract(self.attributes, self.initializable_fields) + + @classmethod + def before_save(cls, instance): + pass + + @classmethod + def after_save(cls, instance): + pass + + def save(self): + self.__class__.before_save(self) + if self.id: # if we have id already, treat this as an update + url = self.url(action='put', params=self.attributes) + response = perform_request('put', url, self.updatable_attributes()) + else: # otherwise, treat this as an insert + url = self.url(action='post', params=self.attributes) + response = perform_request('post', url, self.initializable_attributes()) + self.retrieved = True + self.update_attributes(**response) + self.__class__.after_save(self) + + def delete(self): + url = self.url(action='delete', params=self.attributes) + response = perform_request('delete', url) + self.retrieved = True + self.update_attributes(**response) + + @classmethod + def url_with_id(cls, params={}): + return cls.base_url + '/' + str(params['id']) + + @classmethod + def url_without_id(cls, params={}): + return cls.base_url + + @classmethod + def url(cls, action, params={}): + if cls.base_url is None: + raise CommentClientError("Must provide base_url when using default url function") + if action not in cls.DEFAULT_ACTIONS: + raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS))) + elif action in cls.DEFAULT_ACTIONS_WITH_ID: + try: + return cls.url_with_id(params) + except KeyError: + raise CommentClientError("Cannot perform action {0} without id".format(action)) + else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now + return cls.url_without_id() From 53257a3c478b8c01603365b70d465a61e4747d4c Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 Mar 2013 12:47:28 -0400 Subject: [PATCH 11/42] i need more sleep: --- common/models.py | 131 ----------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 common/models.py diff --git a/common/models.py b/common/models.py deleted file mode 100644 index 2a8992554d..0000000000 --- a/common/models.py +++ /dev/null @@ -1,131 +0,0 @@ -from utils import * - - -class Model(object): - - accessible_fields = ['id'] - updatable_fields = ['id'] - initializable_fields = ['id'] - base_url = None - default_retrieve_params = {} - - DEFAULT_ACTIONS_WITH_ID = ['get', 'put', 'delete'] - DEFAULT_ACTIONS_WITHOUT_ID = ['get_all', 'post'] - DEFAULT_ACTIONS = DEFAULT_ACTIONS_WITH_ID + DEFAULT_ACTIONS_WITHOUT_ID - - def __init__(self, *args, **kwargs): - self.attributes = extract(kwargs, self.accessible_fields) - self.retrieved = False - - def __getattr__(self, name): - if name == 'id': - return self.attributes.get('id', None) - try: - return self.attributes[name] - except KeyError: - if self.retrieved or self.id is None: - raise AttributeError("Field {0} does not exist".format(name)) - self.retrieve() - return self.__getattr__(name) - - def __setattr__(self, name, value): - if name == 'attributes' or name not in self.accessible_fields: - super(Model, self).__setattr__(name, value) - else: - self.attributes[name] = value - - def __getitem__(self, key): - if key not in self.accessible_fields: - raise KeyError("Field {0} does not exist".format(key)) - return self.attributes.get(key) - - def __setitem__(self, key, value): - if key not in self.accessible_fields: - raise KeyError("Field {0} does not exist".format(key)) - self.attributes.__setitem__(key, value) - - def items(self, *args, **kwargs): - return self.attributes.items(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.attributes.get(*args, **kwargs) - - def to_dict(self): - self.retrieve() - return self.attributes - - def retrieve(self, *args, **kwargs): - if not self.retrieved: - self._retrieve(*args, **kwargs) - self.retrieved = True - return self - - def _retrieve(self, *args, **kwargs): - url = self.url(action='get', params=self.attributes) - response = perform_request('get', url, self.default_retrieve_params) - self.update_attributes(**response) - - @classmethod - def find(cls, id): - return cls(id=id) - - def update_attributes(self, *args, **kwargs): - for k, v in kwargs.items(): - if k in self.accessible_fields: - self.__setattr__(k, v) - else: - raise AttributeError("Field {0} does not exist".format(k)) - - def updatable_attributes(self): - return extract(self.attributes, self.updatable_fields) - - def initializable_attributes(self): - return extract(self.attributes, self.initializable_fields) - - @classmethod - def before_save(cls, instance): - pass - - @classmethod - def after_save(cls, instance): - pass - - def save(self): - self.__class__.before_save(self) - if self.id: # if we have id already, treat this as an update - url = self.url(action='put', params=self.attributes) - response = perform_request('put', url, self.updatable_attributes()) - else: # otherwise, treat this as an insert - url = self.url(action='post', params=self.attributes) - response = perform_request('post', url, self.initializable_attributes()) - self.retrieved = True - self.update_attributes(**response) - self.__class__.after_save(self) - - def delete(self): - url = self.url(action='delete', params=self.attributes) - response = perform_request('delete', url) - self.retrieved = True - self.update_attributes(**response) - - @classmethod - def url_with_id(cls, params={}): - return cls.base_url + '/' + str(params['id']) - - @classmethod - def url_without_id(cls, params={}): - return cls.base_url - - @classmethod - def url(cls, action, params={}): - if cls.base_url is None: - raise CommentClientError("Must provide base_url when using default url function") - if action not in cls.DEFAULT_ACTIONS: - raise ValueError("Invalid action {0}. The supported action must be in {1}".format(action, str(cls.DEFAULT_ACTIONS))) - elif action in cls.DEFAULT_ACTIONS_WITH_ID: - try: - return cls.url_with_id(params) - except KeyError: - raise CommentClientError("Cannot perform action {0} without id".format(action)) - else: # action must be in DEFAULT_ACTIONS_WITHOUT_ID now - return cls.url_without_id() From 90213d483cb0af36ca5fd33aa2c3864c6ec8d0e0 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:25:36 -0400 Subject: [PATCH 12/42] Wrote lettuce tests for drop-down, multiple choice, and checkbox problems. --- common/djangoapps/terrain/steps.py | 15 +++- lms/djangoapps/courseware/features/common.py | 10 ++- .../courseware/features/courseware.feature | 2 +- .../features/high-level-tabs.feature | 2 +- .../courseware/features/problems.feature | 53 +++++++++++++ .../courseware/features/problems.py | 79 +++++++++++++++++++ .../courseware/features/registration.feature | 4 +- .../courseware/features/registration.py | 11 +-- 8 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 lms/djangoapps/courseware/features/problems.feature create mode 100644 lms/djangoapps/courseware/features/problems.py diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 3dcef9b1ed..330740b6b3 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -116,6 +116,11 @@ def scroll_to_bottom(): @world.absorb def create_user(uname): + + # If the user already exists, don't try to create it again + if len(User.objects.filter(username=uname)) > 0: + return + portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') portal_user.set_password('test') portal_user.save() @@ -133,13 +138,17 @@ def log_in(email, password): world.browser.visit(django_url('/')) world.browser.is_element_present_by_css('header.global', 10) world.browser.click_link_by_href('#login-modal') + + # wait for the login dialog to load + assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) + 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('email').type(email) + login_form.find_by_name('password').type(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) + assert world.browser.is_element_present_by_css('.content-wrapper', wait_time=10) @world.absorb diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 2e19696ad4..145a56e183 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -81,11 +81,15 @@ 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): +@step(u'I am registered for the course "([^"]*)"$') +def i_am_registered_for_the_course(step, course_id): world.create_user('robot') u = User.objects.get(username='robot') - CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall') + + # If the user is not already enrolled, enroll the user. + if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0: + CourseEnrollment.objects.create(user=u, course_id=course_id) + world.log_in('robot@edx.org', 'test') diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature index 279e5732c9..14e7786fc9 100644 --- a/lms/djangoapps/courseware/features/courseware.feature +++ b/lms/djangoapps/courseware/features/courseware.feature @@ -4,7 +4,7 @@ Feature: View the Courseware Tab 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 + Given I am registered for the course "MITx/6.002x/2013_Spring" And I log in And I click on View Courseware When I click on the "Courseware" tab diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 2e9c4f1886..354376b154 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -8,7 +8,7 @@ Feature: All the high level tabs should work # 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 + Given I am registered for the course "MITx/6.002x/2013_Spring" And I log in And I click on View Courseware When I click on the "Courseware" tab diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature new file mode 100644 index 0000000000..cc459fa35f --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.feature @@ -0,0 +1,53 @@ +Feature: Answer choice problems + As a student in an edX course + In order to test my understanding of the material + I want to answer choice based problems + + Scenario: I can answer a problem correctly + Given I am viewing a "" problem + When I answer a "" problem "correctly" + Then My "" answer is marked "correct" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + Scenario: I can answer a problem incorrectly + Given I am viewing a "" problem + When I answer a "" problem "incorrectly" + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + Scenario: I can submit a blank answer + Given I am viewing a "" problem + When I check a problem + Then My "" answer is marked "incorrect" + + Examples: + | ProblemType | + | drop down | + | multiple choice | + | checkbox | + + + Scenario: I can reset a problem + Given I am viewing a "" problem + And I answer a "" problem "ly" + When I reset the problem + Then My "" answer is marked "unanswered" + + Examples: + | ProblemType | Correctness | + | drop down | correct | + | drop down | incorrect | + | multiple choice | correct | + | multiple choice | incorrect | + | checkbox | correct | + | checkbox | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py new file mode 100644 index 0000000000..4758e16b8d --- /dev/null +++ b/lms/djangoapps/courseware/features/problems.py @@ -0,0 +1,79 @@ +from lettuce import world, step +from lettuce.django import django_url +from selenium.webdriver.support.ui import Select +from common import i_am_registered_for_the_course + +problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', + 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', + 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', } + +@step(u'I am viewing a "([^"]*)" problem') +def view_problem(step, problem_type): + i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') + url = django_url(problem_urls[problem_type]) + world.browser.visit(url) + +@step(u'I answer a "([^"]*)" problem "([^"]*)ly"') +def answer_problem(step, problem_type, correctness): + assert(correctness in ['correct', 'incorrect']) + + if problem_type == "drop down": + select_name = "input_i4x-edX-model_course-problem-Drop_Down_Problem_2_1" + option_text = 'Option 2' if correctness == 'correct' else 'Option 3' + world.browser.select(select_name, option_text) + + elif problem_type == "multiple choice": + if correctness == 'correct': + world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + else: + world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + + elif problem_type == "checkbox": + if correctness == 'correct': + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + else: + world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + + check_problem(step) + +@step(u'I check a problem') +def check_problem(step): + world.browser.find_by_css("input.check").click() + +@step(u'I reset the problem') +def reset_problem(step): + world.browser.find_by_css('input.reset').click() + +@step(u'My "([^"]*)" answer is marked "([^"]*)"') +def assert_answer_mark(step, problem_type, correctness): + assert(correctness in ['correct', 'incorrect', 'unanswered']) + + if problem_type == "multiple choice": + if correctness == 'unanswered': + mark_classes = ['.choicegroup_correct', '.choicegroup_incorrect', + '.correct', '.incorrect'] + for css in mark_classes: + assert(world.browser.is_element_not_present_by_css(css)) + + else: + if correctness == 'correct': + mark_class = '.choicegroup_correct' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + + else: + # Two ways to be marked incorrect: either applying a + # class to the label (marking a particular option) + # or applying a class to a span (marking the whole problem incorrect) + mark_classes = ['.choicegroup_incorrect', '.incorrect'] + assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or + world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + + else: + if correctness == 'unanswered': + assert(world.browser.is_element_not_present_by_css('.correct')) + assert(world.browser.is_element_not_present_by_css('.incorrect')) + + else: + mark_class = '.correct' if correctness == 'correct' else '.incorrect' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature index d9b588534b..890beec1d8 100644 --- a/lms/djangoapps/courseware/features/registration.feature +++ b/lms/djangoapps/courseware/features/registration.feature @@ -6,11 +6,11 @@ Feature: Register for a course 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" + When I register for the course "MITx/6.002x/2013_Spring" 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 + Given I am registered for the course "MITx/6.002x/2013_Spring" And I visit the dashboard When I click the link with the text "Unregister" And I press the "Unregister" button in the Unenroll dialog diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index f585136412..5535319f15 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,12 +1,9 @@ from lettuce import world, step +from lettuce.django import django_url - -@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"] > div' % course - course_link = courses_section.find_by_css(course_link_css).first - course_link.click() +@step('I register for the course "([^"]*)"$') +def i_register_for_the_course(step, course_id): + world.browser.visit(django_url('courses/%s/about' % course_id)) intro_section = world.browser.find_by_css('section.intro') register_link = intro_section.find_by_css('a.register') From 56a6363d7ae95367ea09a985624eb09ce3178f70 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:40:37 -0400 Subject: [PATCH 13/42] CSS selectors in lettuce tests for problems now include the element tag. --- lms/djangoapps/courseware/features/problems.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 4758e16b8d..55467378f4 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -51,8 +51,8 @@ def assert_answer_mark(step, problem_type, correctness): if problem_type == "multiple choice": if correctness == 'unanswered': - mark_classes = ['.choicegroup_correct', '.choicegroup_incorrect', - '.correct', '.incorrect'] + mark_classes = ['label.choicegroup_correct', 'label.choicegroup_incorrect', + 'span.correct', 'span.incorrect'] for css in mark_classes: assert(world.browser.is_element_not_present_by_css(css)) @@ -65,15 +65,15 @@ def assert_answer_mark(step, problem_type, correctness): # Two ways to be marked incorrect: either applying a # class to the label (marking a particular option) # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['.choicegroup_incorrect', '.incorrect'] + mark_classes = ['label.choicegroup_incorrect', 'label.incorrect'] assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) else: if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('.correct')) - assert(world.browser.is_element_not_present_by_css('.incorrect')) + assert(world.browser.is_element_not_present_by_css('span.correct')) + assert(world.browser.is_element_not_present_by_css('span.incorrect')) else: - mark_class = '.correct' if correctness == 'correct' else '.incorrect' + mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) From e6466fdddc15c1e4fd9fc4993140deaddf001c8e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 14:44:27 -0400 Subject: [PATCH 14/42] Changed user creation so that it creates the user only if it doesn't already exist Updated login dialog handling to workaround multiple login dialogs that sometimes appear on a page (where the first one is hidden) --- common/djangoapps/terrain/steps.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 330740b6b3..5917d171b9 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -142,9 +142,14 @@ def log_in(email, password): # wait for the login dialog to load assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) - login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').type(email) - login_form.find_by_name('password').type(password) + # For some reason, the page sometimes includes two #login_form + # elements, the first of which is not visible. + # To avoid this, we always select the last of the two #login_form + # dialogs + login_form = world.browser.find_by_css('form#login_form').last + + 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 From b3946828c034d567b8751c7aa50319edad485cc4 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 16:18:27 -0400 Subject: [PATCH 15/42] Added lettuce tests for string problems --- .../courseware/features/problems.feature | 5 +++++ lms/djangoapps/courseware/features/problems.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index cc459fa35f..f50d8329b6 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -13,6 +13,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -24,6 +25,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -35,6 +37,7 @@ Feature: Answer choice problems | drop down | | multiple choice | | checkbox | + | string | Scenario: I can reset a problem @@ -51,3 +54,5 @@ Feature: Answer choice problems | multiple choice | incorrect | | checkbox | correct | | checkbox | incorrect | + | string | correct | + | string | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 55467378f4..19c0195b15 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -5,7 +5,8 @@ from common import i_am_registered_for_the_course problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', - 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', } + 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', + 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems' } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -35,6 +36,11 @@ def answer_problem(step, problem_type, correctness): else: world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + elif problem_type == 'string': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") + textvalue = 'correct string' if correctness == 'correct' else 'incorrect' + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -65,10 +71,18 @@ def assert_answer_mark(step, problem_type, correctness): # Two ways to be marked incorrect: either applying a # class to the label (marking a particular option) # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['label.choicegroup_incorrect', 'label.incorrect'] + mark_classes = ['label.choicegroup_incorrect', 'span.incorrect'] assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + elif problem_type == "string": + if correctness == 'unanswered': + assert(world.browser.is_element_not_present_by_css('div.correct')) + assert(world.browser.is_element_not_present_by_css('div.incorrect')) + else: + mark_class = 'div.correct' if correctness == 'correct' else 'div.incorrect' + assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + else: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('span.correct')) From 4d5a8e757c883918b32974e66e7958fb434caff2 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 17:01:34 -0400 Subject: [PATCH 16/42] Added lettuce tests for numerical problem --- .../courseware/features/problems.feature | 5 +++++ .../courseware/features/problems.py | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index f50d8329b6..a1e4712f6a 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -14,6 +14,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -26,6 +27,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -38,6 +40,7 @@ Feature: Answer choice problems | multiple choice | | checkbox | | string | + | numerical | Scenario: I can reset a problem @@ -56,3 +59,5 @@ Feature: Answer choice problems | checkbox | incorrect | | string | correct | | string | incorrect | + | numerical | correct | + | numerical | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 19c0195b15..666182684c 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -1,12 +1,14 @@ from lettuce import world, step from lettuce.django import django_url from selenium.webdriver.support.ui import Select +import random from common import i_am_registered_for_the_course problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', - 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems' } + 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', + 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -25,22 +27,27 @@ def answer_problem(step, problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() else: - world.browser.find_by_css("#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() elif problem_type == "checkbox": if correctness == 'correct': - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() else: - world.browser.find_by_css('#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() elif problem_type == 'string': textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") textvalue = 'correct string' if correctness == 'correct' else 'incorrect' textfield.fill(textvalue) + elif problem_type == 'numerical': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Numerical_Problem_2_1") + textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -75,7 +82,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) - elif problem_type == "string": + elif problem_type in ["string", "numerical"]: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('div.correct')) assert(world.browser.is_element_not_present_by_css('div.incorrect')) From 5a2a4055f8bb84fff9bf53888e8b23423f7d6703 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 13 Mar 2013 17:18:02 -0400 Subject: [PATCH 17/42] Added lettuce test for formula problem --- lms/djangoapps/courseware/features/problems.feature | 5 +++++ lms/djangoapps/courseware/features/problems.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index a1e4712f6a..12458537d0 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -15,6 +15,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -28,6 +29,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -41,6 +43,7 @@ Feature: Answer choice problems | checkbox | | string | | numerical | + | formula | Scenario: I can reset a problem @@ -61,3 +64,5 @@ Feature: Answer choice problems | string | incorrect | | numerical | correct | | numerical | incorrect | + | formula | correct | + | formula | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 666182684c..c1b634783d 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -8,7 +8,8 @@ problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/ 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', - 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', } + 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', + 'formula': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Formula_Problems', } @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -48,6 +49,11 @@ def answer_problem(step, problem_type, correctness): textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) textfield.fill(textvalue) + elif problem_type == 'formula': + textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Formula_Problem_2_1") + textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' + textfield.fill(textvalue) + check_problem(step) @step(u'I check a problem') @@ -82,7 +88,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) - elif problem_type in ["string", "numerical"]: + elif problem_type in ["string", "numerical", "formula"]: if correctness == 'unanswered': assert(world.browser.is_element_not_present_by_css('div.correct')) assert(world.browser.is_element_not_present_by_css('div.incorrect')) From 3d8625da9ceed06dcc8057edd6b4b6bf363f5e7e Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 14 Mar 2013 08:42:35 -0400 Subject: [PATCH 18/42] Refactored problem lettuce test implementation --- .../courseware/features/problems.py | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index c1b634783d..1985847bd3 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -4,17 +4,10 @@ from selenium.webdriver.support.ui import Select import random from common import i_am_registered_for_the_course -problem_urls = { 'drop down': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Drop_Down_Problems', - 'multiple choice': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Multiple_Choice_Problems', - 'checkbox': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Checkbox_Problems', - 'string': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/String_Problems', - 'numerical': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Numerical_Problems', - 'formula': '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/Formula_Problems', } - @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') - url = django_url(problem_urls[problem_type]) + url = django_url(problem_url(problem_type)) world.browser.visit(url) @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') @@ -28,31 +21,28 @@ def answer_problem(step, problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_3").check() + inputfield('multiple choice', choice='choice_3').check() else: - world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Multiple_Choice_Problem_2_1_choice_choice_2").check() + inputfield('multiple choice', choice='choice_2').check() elif problem_type == "checkbox": if correctness == 'correct': - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_0').check() - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_2').check() + inputfield('checkbox', choice='choice_0').check() + inputfield('checkbox', choice='choice_2').check() else: - world.browser.find_by_css('input#input_i4x-edX-model_course-problem-Checkbox_Problem_2_1_choice_3').check() + inputfield('checkbox', choice='choice_3').check() elif problem_type == 'string': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-String_Problem_2_1") textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - textfield.fill(textvalue) + inputfield('string').fill(textvalue) elif problem_type == 'numerical': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Numerical_Problem_2_1") textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) - textfield.fill(textvalue) + inputfield('numerical').fill(textvalue) elif problem_type == 'formula': - textfield = world.browser.find_by_css("input#input_i4x-edX-model_course-problem-Formula_Problem_2_1") textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - textfield.fill(textvalue) + inputfield('formula').fill(textvalue) check_problem(step) @@ -104,3 +94,35 @@ def assert_answer_mark(step, problem_type, correctness): else: mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + +def problem_url(problem_type): + base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' + url_extensions = { 'drop down': 'Drop_Down_Problems', + 'multiple choice': 'Multiple_Choice_Problems', + 'checkbox': 'Checkbox_Problems', + 'string': 'String_Problems', + 'numerical': 'Numerical_Problems', + 'formula': 'Formula_Problems', } + + assert(problem_type in url_extensions) + return base + url_extensions[problem_type] + + + +def inputfield(problem_type, choice=None): + field_extensions = { 'drop down': 'Drop_Down_Problem', + 'multiple choice': 'Multiple_Choice_Problem', + 'checkbox': 'Checkbox_Problem', + 'string': 'String_Problem', + 'numerical': 'Numerical_Problem', + 'formula': 'Formula_Problem', } + + assert(problem_type in field_extensions) + extension = field_extensions[problem_type] + sel = "input#input_i4x-edX-model_course-problem-%s_2_1" % extension + + if choice is not None: + base = "_choice_" if problem_type == "multiple choice" else "_" + sel = sel + base + str(choice) + + return world.browser.find_by_css(sel) From a20a3a02bb4b46ce7df6f6dd27637a8c430efaaa Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 14 Mar 2013 09:15:24 -0400 Subject: [PATCH 19/42] Refactored lettuce problem test assertion that a problem is correct/incorrect/unanswered --- .../courseware/features/problems.py | 100 ++++++++++++------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 1985847bd3..686fc8c7a1 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -12,6 +12,12 @@ def view_problem(step, problem_type): @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') def answer_problem(step, problem_type, correctness): + """ Mark a given problem type correct or incorrect, then submit it. + + *problem_type* is a string representing the type of problem (e.g. 'drop down') + *correctness* is in ['correct', 'incorrect'] + """ + assert(correctness in ['correct', 'incorrect']) if problem_type == "drop down": @@ -44,6 +50,7 @@ def answer_problem(step, problem_type, correctness): textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' inputfield('formula').fill(textvalue) + # Submit the problem check_problem(step) @step(u'I check a problem') @@ -56,46 +63,70 @@ def reset_problem(step): @step(u'My "([^"]*)" answer is marked "([^"]*)"') def assert_answer_mark(step, problem_type, correctness): + """ Assert that the expected answer mark is visible for a given problem type. + + *problem_type* is a string identifying the type of problem (e.g. 'drop down') + *correctness* is in ['correct', 'incorrect', 'unanswered'] + + Asserting that a problem is marked 'unanswered' means that + the problem is NOT marked correct and NOT marked incorrect. + This can occur, for example, if the user has reset the problem. """ + + # Dictionaries that map problem types to the css selectors + # for correct/incorrect marks. + # The elements are lists of selectors because a particular problem type + # might be marked in multiple ways. + # For example, multiple choice is marked incorrect differently + # depending on whether the user selects an incorrect + # item or submits without selecting any item) + correct_selectors = { 'drop down': ['span.correct'], + 'multiple choice': ['label.choicegroup_correct'], + 'checkbox': ['span.correct'], + 'string': ['div.correct'], + 'numerical': ['div.correct'], + 'formula': ['div.correct'], } + + incorrect_selectors = { 'drop down': ['span.incorrect'], + 'multiple choice': ['label.choicegroup_incorrect', + 'span.incorrect'], + 'checkbox': ['span.incorrect'], + 'string': ['div.incorrect'], + 'numerical': ['div.incorrect'], + 'formula': ['div.incorrect'], } + assert(correctness in ['correct', 'incorrect', 'unanswered']) + assert(problem_type in correct_selectors and problem_type in incorrect_selectors) - if problem_type == "multiple choice": - if correctness == 'unanswered': - mark_classes = ['label.choicegroup_correct', 'label.choicegroup_incorrect', - 'span.correct', 'span.incorrect'] - for css in mark_classes: - assert(world.browser.is_element_not_present_by_css(css)) - - else: - if correctness == 'correct': - mark_class = '.choicegroup_correct' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + # Assert that the question has the expected mark + # (either correct or incorrect) + if correctness in ["correct", "incorrect"]: - else: - # Two ways to be marked incorrect: either applying a - # class to the label (marking a particular option) - # or applying a class to a span (marking the whole problem incorrect) - mark_classes = ['label.choicegroup_incorrect', 'span.incorrect'] - assert(world.browser.is_element_present_by_css(mark_classes[0], wait_time=4) or - world.browser.is_element_present_by_css(mark_classes[1], wait_time=4)) + selector_dict = correct_selectors if correctness == "correct" else incorrect_selectors - elif problem_type in ["string", "numerical", "formula"]: - if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('div.correct')) - assert(world.browser.is_element_not_present_by_css('div.incorrect')) - else: - mark_class = 'div.correct' if correctness == 'correct' else 'div.incorrect' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) + # At least one of the correct selectors should be present + for sel in selector_dict[problem_type]: + has_expected_mark = world.browser.is_element_present_by_css(sel, wait_time=4) + # As soon as we find the selector, break out of the loop + if has_expected_mark: + break + + # Expect that we found the right mark (correct or incorrect) + assert(has_expected_mark) + + # Assert that the question has neither correct nor incorrect + # because it is unanswered (possibly reset) else: - if correctness == 'unanswered': - assert(world.browser.is_element_not_present_by_css('span.correct')) - assert(world.browser.is_element_not_present_by_css('span.incorrect')) + # Get all the correct/incorrect selectors for this problem type + selector_list = correct_selectors[problem_type] + incorrect_selectors[problem_type] + + # Assert that none of the correct/incorrect selectors are present + for sel in selector_list: + assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) - else: - mark_class = 'span.correct' if correctness == 'correct' else 'span.incorrect' - assert(world.browser.is_element_present_by_css(mark_class, wait_time=4)) def problem_url(problem_type): + """ Construct a url to a page with the given problem type """ base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' url_extensions = { 'drop down': 'Drop_Down_Problems', 'multiple choice': 'Multiple_Choice_Problems', @@ -110,6 +141,13 @@ def problem_url(problem_type): def inputfield(problem_type, choice=None): + """ Return the element for *problem_type*. + For example, if problem_type is 'string', return + the text field for the string problem in the test course. + + *choice* is the name of the checkbox input in a group + of checkboxes. """ + field_extensions = { 'drop down': 'Drop_Down_Problem', 'multiple choice': 'Multiple_Choice_Problem', 'checkbox': 'Checkbox_Problem', From 8423816076f48539a5035f3b4a011bbecd3c52ff Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 11:33:04 -0400 Subject: [PATCH 20/42] LMS contentstore lettuce tests now dynamically create courses in mongo using terrain.factories.py and capa.tests.response_xml_factory --- common/djangoapps/terrain/factories.py | 33 +++- common/djangoapps/terrain/steps.py | 28 ++-- .../capa/capa/tests/response_xml_factory.py | 8 +- lms/djangoapps/courseware/features/common.py | 77 +++++++++- .../courseware/features/courseware.feature | 11 -- .../features/high-level-tabs.feature | 33 ++-- .../courseware/features/problems.py | 142 ++++++++++++++---- .../courseware/features/registration.feature | 7 +- .../courseware/features/registration.py | 7 +- lms/envs/acceptance.py | 24 ++- 10 files changed, 272 insertions(+), 98 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/courseware.feature diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index a531f4fd26..5fa88e4b1d 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -121,21 +121,41 @@ class XModuleItemFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): """ - kwargs must include parent_location, template. Can contain display_name - target_class is ignored + Uses *kwargs*: + + *parent_location* (required): the location of the parent module + (e.g. the parent course or section) + + *template* (required): the template to create the item from + (e.g. i4x://templates/section/Empty) + + *data* (optional): the data for the item + (e.g. XML problem definition for a problem item) + + *display_name* (optional): the display name of the item + + *metadata* (optional): dictionary of metadata attributes + + *target_class* is ignored """ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] parent_location = Location(kwargs.get('parent_location')) template = Location(kwargs.get('template')) + data = kwargs.get('data') display_name = kwargs.get('display_name') + metadata = kwargs.get('metadata', {}) store = modulestore('direct') # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) - dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + # If a display name is set, use that + dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex + dest_location = parent_location._replace(category=template.category, + name=dest_name) new_item = store.clone_item(template, dest_location) @@ -143,8 +163,15 @@ class XModuleItemFactory(Factory): if display_name is not None: new_item.display_name = display_name + # Add additional metadata or override current metadata + new_item.metadata.update(metadata) + store.update_metadata(new_item.location.url(), own_metadata(new_item)) + # replace the data with the optional *data* parameter + if data is not None: + store.update_item(new_item.location, data) + if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 5917d171b9..1f90113f46 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -69,6 +69,11 @@ def the_page_title_should_be(step, title): assert_equals(world.browser.title, title) +@step(u'the page title should contain "([^"]*)"$') +def the_page_title_should_contain(step, title): + assert(title in world.browser.title) + + @step('I am a logged in user$') def i_am_logged_in_user(step): create_user('robot') @@ -80,18 +85,6 @@ 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) @@ -139,13 +132,16 @@ def log_in(email, password): world.browser.is_element_present_by_css('header.global', 10) world.browser.click_link_by_href('#login-modal') - # wait for the login dialog to load - assert(world.browser.is_element_present_by_css('form#login_form', wait_time=10)) + # Wait for the login dialog to load + # This is complicated by the fact that sometimes a second #login_form + # dialog loads, while the first one remains hidden. + # We give them both time to load, starting with the second one. + world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=2) + world.browser.is_element_present_by_css('form#login_form', wait_time=2) # For some reason, the page sometimes includes two #login_form # elements, the first of which is not visible. - # To avoid this, we always select the last of the two #login_form - # dialogs + # To avoid this, we always select the last of the two #login_form dialogs login_form = world.browser.find_by_css('form#login_form').last login_form.find_by_name('email').fill(email) diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 7aa299d20d..08ed1ca668 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -151,13 +151,11 @@ class ResponseXMLFactory(object): choice_element = etree.SubElement(group_element, "choice") choice_element.set("correct", "true" if correct_val else "false") - # Add some text describing the choice - etree.SubElement(choice_element, "startouttext") - etree.text = "Choice description" - etree.SubElement(choice_element, "endouttext") - # Add a name identifying the choice, if one exists + # For simplicity, we use the same string as both the + # name attribute and the text of the element if name: + choice_element.text = str(name) choice_element.set("name", str(name)) return group_element diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 145a56e183..4f307511df 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -5,6 +5,10 @@ from lettuce.django import django_url from django.conf import settings from django.contrib.auth.models import User from student.models import CourseEnrollment +from terrain.factories import CourseFactory, ItemFactory +from xmodule.modulestore import Location +from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.templates import update_templates import time from logging import getLogger @@ -81,17 +85,53 @@ def i_am_not_logged_in(step): world.browser.cookies.delete() +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_SECTION_NAME = "Problem" + +@step(u'The course "([^"]*)" exists$') +def create_course(step, course): + + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + flush_xmodule_store() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = CourseFactory.create(org=TEST_COURSE_ORG, + number=course, + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section = ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME) + + problem_section = ItemFactory.create(parent_location=section.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME) + @step(u'I am registered for the course "([^"]*)"$') -def i_am_registered_for_the_course(step, course_id): +def i_am_registered_for_the_course(step, course): + # Create the course + create_course(step, course) + + # Create the user world.create_user('robot') u = User.objects.get(username='robot') # If the user is not already enrolled, enroll the user. - if len(CourseEnrollment.objects.filter(user=u, course_id=course_id)) == 0: - CourseEnrollment.objects.create(user=u, course_id=course_id) + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course)) world.log_in('robot@edx.org', 'test') +@step(u'The course "([^"]*)" has extra tab "([^"]*)"$') +def add_tab_to_course(step, course, extra_tab_name): + section_item = ItemFactory.create(parent_location=course_location(course), + template="i4x://edx/templates/static_tab/Empty", + display_name=str(extra_tab_name)) + @step(u'I am an edX user$') def i_am_an_edx_user(step): @@ -101,3 +141,34 @@ def i_am_an_edx_user(step): @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): world.create_user(uname) + + +def flush_xmodule_store(): + # Flush and initialize the module store + # It needs the templates because it creates new records + # by cloning from the template. + # Note that if your test module gets in some weird state + # (though it shouldn't), do this manually + # from the bash shell to drop it: + # $ mongo test_xmodule --eval "db.dropDatabase()" + _MODULESTORES = {} + modulestore().collection.drop() + update_templates() + +def course_id(course_num): + return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, + TEST_COURSE_NAME.replace(" ", "_")) + +def course_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='course', + name=TEST_COURSE_NAME.replace(" ", "_")) + +def section_location(course_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='sequential', + name=TEST_SECTION_NAME.replace(" ", "_")) diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature deleted file mode 100644 index 14e7786fc9..0000000000 --- a/lms/djangoapps/courseware/features/courseware.feature +++ /dev/null @@ -1,11 +0,0 @@ -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 the course "MITx/6.002x/2013_Spring" - And I log in - And I click on View Courseware - When I click on the "Courseware" tab - Then the "Courseware" tab is active diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 354376b154..102f752e1f 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,21 +3,18 @@ Feature: All the high level tabs should work 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 the course "MITx/6.002x/2013_Spring" - 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" +Scenario: I can navigate to all high-level tabs in a course + Given: I am registered for the course "6.002x" + And The course "6.002x" has extra tab "Custom Tab" + And I log in + And I click on View Courseware + When I click on the "" tab + Then the page title should contain "" + + Examples: + | TabName | PageTitle | + | Courseware | 6.002x Courseware | + | Course Info | 6.002x Course Info | + | Custom Tab | 6.002x Custom Tab | + | Wiki | edX Wiki | + | Progress | 6.002x Progress | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 686fc8c7a1..3af4843c3c 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,14 +2,118 @@ from lettuce import world, step from lettuce.django import django_url from selenium.webdriver.support.ui import Select import random -from common import i_am_registered_for_the_course +import textwrap +from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location +from terrain.factories import ItemFactory +from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ + ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \ + StringResponseXMLFactory, NumericalResponseXMLFactory, \ + FormulaResponseXMLFactory, CustomResponseXMLFactory + +# Factories from capa.tests.response_xml_factory that we will use +# to generate the problem XML, with the keyword args used to configure +# the output. +problem_factory_dict = { + 'drop down': { + 'factory': OptionResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Option 2', + 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'], + 'correct_option': 'Option 2'}}, + + 'multiple choice': { + 'factory': MultipleChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choice 3', + 'choices': [False, False, True, False], + 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}}, + + 'checkbox': { + 'factory': ChoiceResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The correct answer is Choices 1 and 3', + 'choice_type':'checkbox', + 'choices':[True, False, True, False, False], + 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, + + 'string': { + 'factory': StringResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is "correct string"', + 'case_sensitive': False, + 'answer': 'correct string' }}, + + 'numerical': { + 'factory': NumericalResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The answer is pi + 1', + 'answer': '4.14159', + 'tolerance': '0.00001', + 'math_display': True }}, + + 'formula': { + 'factory': FormulaResponseXMLFactory(), + 'kwargs': { + 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', + 'sample_dict': {'x': (-100, 100), 'y': (-100, 100) }, + 'num_samples': 10, + 'tolerance': 0.00001, + 'math_display': True, + 'answer': 'x^2+2*x+y'}}, + + 'script': { + 'factory': CustomResponseXMLFactory(), + 'kwargs': { + 'question_text': 'Enter two integers that sum to 10.', + 'cfn': 'test_add_to_ten', + 'expect': '10', + 'num_inputs': 2, + 'script': textwrap.dedent(""" + def test_add_to_ten(expect,ans): + try: + a1=int(ans[0]) + a2=int(ans[1]) + except ValueError: + a1=0 + a2=0 + return (a1+a2)==int(expect) + """) }}, + } + +def add_problem_to_course(course, problem_type): + + assert(problem_type in problem_factory_dict) + + # Generate the problem XML using capa.tests.response_xml_factory + factory_dict = problem_factory_dict[problem_type] + problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) + + # Create a problem item using our generated XML + # We set rerandomize=always in the metadata so that the "Reset" button + # will appear. + problem_item = ItemFactory.create(parent_location=section_location(course), + template="i4x://edx/templates/problem/Blank_Common_Problem", + display_name=str(problem_type), + data=problem_xml, + metadata={'rerandomize':'always'}) @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): - i_am_registered_for_the_course(step, 'edX/model_course/2013_Spring') - url = django_url(problem_url(problem_type)) + i_am_registered_for_the_course(step, 'model_course') + + # Ensure that the course has this problem type + add_problem_to_course('model_course', problem_type) + + # Go to the one section in the factory-created course + # which should be loaded with the correct problem + chapter_name = TEST_SECTION_NAME.replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + world.browser.visit(url) + @step(u'I answer a "([^"]*)" problem "([^"]*)ly"') def answer_problem(step, problem_type, correctness): """ Mark a given problem type correct or incorrect, then submit it. @@ -21,7 +125,7 @@ def answer_problem(step, problem_type, correctness): assert(correctness in ['correct', 'incorrect']) if problem_type == "drop down": - select_name = "input_i4x-edX-model_course-problem-Drop_Down_Problem_2_1" + select_name = "input_i4x-edx-model_course-problem-drop_down_2_1" option_text = 'Option 2' if correctness == 'correct' else 'Option 3' world.browser.select(select_name, option_text) @@ -125,21 +229,6 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) -def problem_url(problem_type): - """ Construct a url to a page with the given problem type """ - base = '/courses/edX/model_course/2013_Spring/courseware/Problem_Components/' - url_extensions = { 'drop down': 'Drop_Down_Problems', - 'multiple choice': 'Multiple_Choice_Problems', - 'checkbox': 'Checkbox_Problems', - 'string': 'String_Problems', - 'numerical': 'Numerical_Problems', - 'formula': 'Formula_Problems', } - - assert(problem_type in url_extensions) - return base + url_extensions[problem_type] - - - def inputfield(problem_type, choice=None): """ Return the element for *problem_type*. For example, if problem_type is 'string', return @@ -148,19 +237,14 @@ def inputfield(problem_type, choice=None): *choice* is the name of the checkbox input in a group of checkboxes. """ - field_extensions = { 'drop down': 'Drop_Down_Problem', - 'multiple choice': 'Multiple_Choice_Problem', - 'checkbox': 'Checkbox_Problem', - 'string': 'String_Problem', - 'numerical': 'Numerical_Problem', - 'formula': 'Formula_Problem', } - - assert(problem_type in field_extensions) - extension = field_extensions[problem_type] - sel = "input#input_i4x-edX-model_course-problem-%s_2_1" % extension + sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_") if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" sel = sel + base + str(choice) + # If the input element doesn't exist, fail immediately + assert(world.browser.is_element_present_by_css(sel, wait_time=4)) + + # Retrieve the input element return world.browser.find_by_css(sel) diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature index 890beec1d8..5933f860bb 100644 --- a/lms/djangoapps/courseware/features/registration.feature +++ b/lms/djangoapps/courseware/features/registration.feature @@ -4,13 +4,14 @@ Feature: Register for a course I want to register for a class on the edX website Scenario: I can register for a course - Given I am logged in + Given The course "6.002x" exists + And I am logged in And I visit the courses page - When I register for the course "MITx/6.002x/2013_Spring" + When I register for the course "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 the course "MITx/6.002x/2013_Spring" + Given I am registered for the course "6.002x" And I visit the dashboard When I click the link with the text "Unregister" And I press the "Unregister" button in the Unenroll dialog diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 5535319f15..9587842dd6 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -1,9 +1,12 @@ from lettuce import world, step from lettuce.django import django_url +from common import TEST_COURSE_ORG, TEST_COURSE_NAME @step('I register for the course "([^"]*)"$') -def i_register_for_the_course(step, course_id): - world.browser.visit(django_url('courses/%s/about' % course_id)) +def i_register_for_the_course(step, course): + cleaned_name = TEST_COURSE_NAME.replace(' ', '_') + url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name)) + world.browser.visit(url) intro_section = world.browser.find_by_css('section.intro') register_link = intro_section.find_by_css('a.register') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index b6941f4a70..3dac545367 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -8,16 +8,24 @@ from .test import * # 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 +# Use the mongo store for acceptance tests +modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore', + 'fs_root': GITHUB_REPO_ROOT, + 'render_template': 'mitxmako.shortcuts.render_to_string', +} + MODULESTORE = { 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': DATA_DIR, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': modulestore_options } } From e41bb8462cc1796ad07eb5fbc24381665b9701f4 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 12:13:07 -0400 Subject: [PATCH 21/42] Added lettuce tests for script (customresponse) problems. Increased wait time for login screen to reduce false positives. --- common/djangoapps/terrain/steps.py | 2 +- .../courseware/features/problems.feature | 5 ++++ .../courseware/features/problems.py | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 1f90113f46..50fe0faf39 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -136,7 +136,7 @@ def log_in(email, password): # This is complicated by the fact that sometimes a second #login_form # dialog loads, while the first one remains hidden. # We give them both time to load, starting with the second one. - world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=2) + world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=4) world.browser.is_element_present_by_css('form#login_form', wait_time=2) # For some reason, the page sometimes includes two #login_form diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 12458537d0..8828ebc699 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -16,6 +16,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can answer a problem incorrectly Given I am viewing a "" problem @@ -30,6 +31,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can submit a blank answer Given I am viewing a "" problem @@ -44,6 +46,7 @@ Feature: Answer choice problems | string | | numerical | | formula | + | script | Scenario: I can reset a problem @@ -66,3 +69,5 @@ Feature: Answer choice problems | numerical | incorrect | | formula | correct | | formula | incorrect | + | script | correct | + | script | incorrect | diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 3af4843c3c..0b5ecbe20a 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -154,6 +154,19 @@ def answer_problem(step, problem_type, correctness): textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' inputfield('formula').fill(textvalue) + elif problem_type == 'script': + # Correct answer is any two integers that sum to 10 + first_addend = random.randint(-100, 100) + second_addend = 10 - first_addend + + # If we want an incorrect answer, then change + # the second addend so they no longer sum to 10 + if correctness == 'incorrect': + second_addend += random.randint(1, 10) + + inputfield('script', input_num=1).fill(str(first_addend)) + inputfield('script', input_num=2).fill(str(second_addend)) + # Submit the problem check_problem(step) @@ -188,7 +201,8 @@ def assert_answer_mark(step, problem_type, correctness): 'checkbox': ['span.correct'], 'string': ['div.correct'], 'numerical': ['div.correct'], - 'formula': ['div.correct'], } + 'formula': ['div.correct'], + 'script': ['div.correct'], } incorrect_selectors = { 'drop down': ['span.incorrect'], 'multiple choice': ['label.choicegroup_incorrect', @@ -196,7 +210,8 @@ def assert_answer_mark(step, problem_type, correctness): 'checkbox': ['span.incorrect'], 'string': ['div.incorrect'], 'numerical': ['div.incorrect'], - 'formula': ['div.incorrect'], } + 'formula': ['div.incorrect'], + 'script': ['div.incorrect'] } assert(correctness in ['correct', 'incorrect', 'unanswered']) assert(problem_type in correct_selectors and problem_type in incorrect_selectors) @@ -229,7 +244,7 @@ def assert_answer_mark(step, problem_type, correctness): assert(world.browser.is_element_not_present_by_css(sel, wait_time=4)) -def inputfield(problem_type, choice=None): +def inputfield(problem_type, choice=None, input_num=1): """ Return the element for *problem_type*. For example, if problem_type is 'string', return the text field for the string problem in the test course. @@ -237,7 +252,8 @@ def inputfield(problem_type, choice=None): *choice* is the name of the checkbox input in a group of checkboxes. """ - sel = "input#input_i4x-edx-model_course-problem-%s_2_1" % problem_type.replace(" ", "_") + sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % + (problem_type.replace(" ", "_"), str(input_num))) if choice is not None: base = "_choice_" if problem_type == "multiple choice" else "_" From c34666af80706f329f36aa75f134178cc4b77a34 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 14:20:55 -0400 Subject: [PATCH 22/42] Fix version handling --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f05f419a03..db0208ba95 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,6 +8,7 @@ from xmodule.raw_module import RawDescriptor from .x_module import XModule from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor +import copy log = logging.getLogger("mitx.courseware") @@ -137,13 +138,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): student_attributes = [i[4] for i in VERSION_TUPLES] version_error_string = "Could not find version {0}, using version {1} instead" + version_to_use = copy.copy(self.version) try: - version_index = versions.index(self.version) + version_index = versions.index(version_to_use) except: #This is a dev_facing_error - log.error(version_error_string.format(self.version, DEFAULT_VERSION)) - self.version = DEFAULT_VERSION - version_index = versions.index(self.version) + log.error(version_error_string.format(version_to_use, DEFAULT_VERSION)) + version_to_use = DEFAULT_VERSION + version_index = versions.index(version_to_use) self.student_attributes = student_attributes[version_index] self.settings_attributes = settings_attributes[version_index] From 97839d9de5ee003aa6644c6b4d35e2166b1e4f98 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 14:52:55 -0400 Subject: [PATCH 23/42] Fix default version string integer issues --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index db0208ba95..eab091b1aa 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -22,11 +22,10 @@ V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES VERSION_TUPLES = ( - ('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), + (1, CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), ) DEFAULT_VERSION = 1 -DEFAULT_VERSION = str(DEFAULT_VERSION) class CombinedOpenEndedFields(object): From 405ea8d67515e990b286f6c51efc7996e402c215 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 14:57:48 -0400 Subject: [PATCH 24/42] Remove copy.copy --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index eab091b1aa..63d77b5efe 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,7 +8,6 @@ from xmodule.raw_module import RawDescriptor from .x_module import XModule from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor -import copy log = logging.getLogger("mitx.courseware") @@ -137,7 +136,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): student_attributes = [i[4] for i in VERSION_TUPLES] version_error_string = "Could not find version {0}, using version {1} instead" - version_to_use = copy.copy(self.version) + version_to_use = self.version try: version_index = versions.index(version_to_use) except: From b609ab0e6abd1f3123189647569d83ef9d8b0272 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 15:05:47 -0400 Subject: [PATCH 25/42] Convert to using versioninteger class --- .../xmodule/combined_open_ended_module.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 63d77b5efe..b7632ccffa 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -26,6 +26,24 @@ VERSION_TUPLES = ( DEFAULT_VERSION = 1 +class VersionInteger(Integer): + """ + A model type that converts from strings to integers when reading from json. + Also does error checking to see if version is correct or not. + """ + def from_json(self, value): + try: + value = int(value) + versions = [i[0] for i in VERSION_TUPLES] + try: + versions.index(value) + except: + version_error_string = "Could not find version {0}, using version {1} instead" + log.error(version_error_string.format(value, DEFAULT_VERSION)) + value = DEFAULT_VERSION + except: + value = DEFAULT_VERSION + return value class CombinedOpenEndedFields(object): display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) @@ -41,7 +59,7 @@ class CombinedOpenEndedFields(object): due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, scope=Scope.settings) max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings) - version = Integer(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) + version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) @@ -134,16 +152,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): modules = [i[2] for i in VERSION_TUPLES] settings_attributes = [i[3] for i in VERSION_TUPLES] student_attributes = [i[4] for i in VERSION_TUPLES] - version_error_string = "Could not find version {0}, using version {1} instead" - version_to_use = self.version - try: - version_index = versions.index(version_to_use) - except: - #This is a dev_facing_error - log.error(version_error_string.format(version_to_use, DEFAULT_VERSION)) - version_to_use = DEFAULT_VERSION - version_index = versions.index(version_to_use) + version_index = versions.index(self.version) self.student_attributes = student_attributes[version_index] self.settings_attributes = settings_attributes[version_index] From 3a13cd7b34b35a50feba2dd4d5c55944b687263c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 15:09:39 -0400 Subject: [PATCH 26/42] Merged changes in factories.py with version in master --- common/djangoapps/terrain/factories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 5fa88e4b1d..5ea34d1190 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -164,9 +164,9 @@ class XModuleItemFactory(Factory): new_item.display_name = display_name # Add additional metadata or override current metadata - new_item.metadata.update(metadata) - - store.update_metadata(new_item.location.url(), own_metadata(new_item)) + item_metadata = own_metadata(new_item) + item_metadata.update(metadata) + store.update_metadata(new_item.location.url(), item_metadata) # replace the data with the optional *data* parameter if data is not None: From 01b1974f5077b15f6b4da1467fa8da1a3a943466 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 15:15:23 -0400 Subject: [PATCH 27/42] Address review comment --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index b7632ccffa..cd3f66efbc 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -35,9 +35,7 @@ class VersionInteger(Integer): try: value = int(value) versions = [i[0] for i in VERSION_TUPLES] - try: - versions.index(value) - except: + if value not in versions: version_error_string = "Could not find version {0}, using version {1} instead" log.error(version_error_string.format(value, DEFAULT_VERSION)) value = DEFAULT_VERSION From 500c4ab86ea86d9e2c17f5161305bbd8ec099c14 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 15:40:04 -0400 Subject: [PATCH 28/42] Convert versions to tuples --- .../xmodule/combined_open_ended_module.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index cd3f66efbc..981d647d4f 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -8,6 +8,7 @@ from xmodule.raw_module import RawDescriptor from .x_module import XModule from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor +from collections import namedtuple log = logging.getLogger("mitx.courseware") @@ -20,8 +21,9 @@ V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES +VersionTuple= namedtuple('VersionTuple', ['version', 'descriptor', 'module', 'settings_attributes', 'student_attributes']) VERSION_TUPLES = ( - (1, CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), + VersionTuple(1, CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), ) DEFAULT_VERSION = 1 @@ -34,7 +36,7 @@ class VersionInteger(Integer): def from_json(self, value): try: value = int(value) - versions = [i[0] for i in VERSION_TUPLES] + versions = [i.version for i in VERSION_TUPLES] if value not in versions: version_error_string = "Could not find version {0}, using version {1} instead" log.error(version_error_string.format(value, DEFAULT_VERSION)) @@ -145,16 +147,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): if self.task_states is None: self.task_states = [] - versions = [i[0] for i in VERSION_TUPLES] - descriptors = [i[1] for i in VERSION_TUPLES] - modules = [i[2] for i in VERSION_TUPLES] - settings_attributes = [i[3] for i in VERSION_TUPLES] - student_attributes = [i[4] for i in VERSION_TUPLES] + versions = [i.version for i in VERSION_TUPLES] version_index = versions.index(self.version) - self.student_attributes = student_attributes[version_index] - self.settings_attributes = settings_attributes[version_index] + version_tuple = VERSION_TUPLES[version_index] + + self.student_attributes = version_tuple.student_attributes + self.settings_attributes = version_tuple.settings_attributes attributes = self.student_attributes + self.settings_attributes @@ -162,9 +162,9 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): 'rewrite_content_links': self.rewrite_content_links, } instance_state = {k: getattr(self, k) for k in attributes} - self.child_descriptor = descriptors[version_index](self.system) - self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system) - self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor, + self.child_descriptor = version_tuple.descriptor(self.system) + self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system) + self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor, instance_state=instance_state, static_data=static_data, attributes=attributes) self.save_instance_data() From cd3e88fdd5ea21d2da84000f256151da3d220d97 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 16:05:38 -0400 Subject: [PATCH 29/42] Fix version tuples to be a dictionary --- .../xmodule/combined_open_ended_module.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 981d647d4f..8379d9a7e6 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -21,10 +21,10 @@ V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES -VersionTuple= namedtuple('VersionTuple', ['version', 'descriptor', 'module', 'settings_attributes', 'student_attributes']) -VERSION_TUPLES = ( - VersionTuple(1, CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), -) +VersionTuple= namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes']) +VERSION_TUPLES = { + 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), +} DEFAULT_VERSION = 1 @@ -36,8 +36,7 @@ class VersionInteger(Integer): def from_json(self, value): try: value = int(value) - versions = [i.version for i in VERSION_TUPLES] - if value not in versions: + if value not in VERSION_TUPLES: version_error_string = "Could not find version {0}, using version {1} instead" log.error(version_error_string.format(value, DEFAULT_VERSION)) value = DEFAULT_VERSION @@ -147,11 +146,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): if self.task_states is None: self.task_states = [] - versions = [i.version for i in VERSION_TUPLES] - - version_index = versions.index(self.version) - - version_tuple = VERSION_TUPLES[version_index] + version_tuple = VERSION_TUPLES[self.version] self.student_attributes = version_tuple.student_attributes self.settings_attributes = version_tuple.settings_attributes From 5fd1e7426d9fdd46bbbb209d5e04d9ffafc2def6 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 15 Mar 2013 16:17:45 -0400 Subject: [PATCH 30/42] Lettuce tests now import one_time_startup.py to ensure that mongo caches are initialized for the test database. This avoids a warning from the mongo modulestore. --- common/djangoapps/terrain/browser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0881d86124..6a60802d07 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -3,6 +3,11 @@ from splinter.browser import Browser from logging import getLogger import time +# Let the LMS and CMS do their one-time setup +# For example, setting up mongo caches +from lms import one_time_startup +from cms import one_time_startup + logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") @@ -16,7 +21,6 @@ def initial_setup(server): # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') - @before.each_scenario def reset_data(scenario): # Clean out the django test database defined in the From e4efda9a475abe5c5e68f4bbcfd5bd9d6cf6550b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 15 Mar 2013 16:21:36 -0400 Subject: [PATCH 31/42] Reformat code --- .../xmodule/combined_open_ended_module.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 8379d9a7e6..48fbfcced1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -12,27 +12,29 @@ from collections import namedtuple log = logging.getLogger("mitx.courseware") - V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", - "skip_spelling_checks", "due", "graceperiod", "max_score"] + "skip_spelling_checks", "due", "graceperiod", "max_score"] V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", - "student_attempts", "ready_to_reset"] + "student_attempts", "ready_to_reset"] V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES -VersionTuple= namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes']) +VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes']) VERSION_TUPLES = { - 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), + 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, + V1_STUDENT_ATTRIBUTES), } DEFAULT_VERSION = 1 + class VersionInteger(Integer): """ A model type that converts from strings to integers when reading from json. Also does error checking to see if version is correct or not. """ + def from_json(self, value): try: value = int(value) @@ -44,19 +46,26 @@ class VersionInteger(Integer): value = DEFAULT_VERSION return value + class CombinedOpenEndedFields(object): display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state) - state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.student_state) - student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state) - ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, scope=Scope.student_state) + state = String(help="Which step within the current task that the student is on.", default="initial", + scope=Scope.student_state) + student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, + scope=Scope.student_state) + ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, + scope=Scope.student_state) attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) - is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) - accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, scope=Scope.settings) - skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, scope=Scope.settings) + is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) + accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, + scope=Scope.settings) + skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, + scope=Scope.settings) due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) - graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, scope=Scope.settings) + graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, + scope=Scope.settings) max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) data = String(help="XML data for the problem", scope=Scope.content) @@ -160,7 +169,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): self.child_descriptor = version_tuple.descriptor(self.system) self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system) self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor, - instance_state=instance_state, static_data=static_data, attributes=attributes) + instance_state=instance_state, static_data=static_data, + attributes=attributes) self.save_instance_data() def get_html(self): From a4451bdcd269341a82ce8280fe9148e38460c6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Fri, 15 Mar 2013 10:29:30 -0700 Subject: [PATCH 32/42] XBlock SDK announcement LMS Lighthouse [#261] --- lms/templates/feed.rss | 31 +++++---- .../press_releases/xblock_announcement.html | 63 +++++++++++++++++++ lms/urls.py | 6 +- 3 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 lms/templates/static_templates/press_releases/xblock_announcement.html diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index 37c7a8ce6e..a6fda0d20a 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -4,9 +4,18 @@ tag:www.edx.org,2012:/blog - ## + EdX Blog - 2013-03-14T14:00:12-07:00 + 2013-03-15T14:00:12-07:00 + + tag:www.edx.org,2013:Post/16 + 2013-03-15T10:00:00-07:00 + 2013-03-15T10:00:00-07:00 + + edX releases XBlock SDK, first step toward open source vision + <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> + <p></p> + tag:www.edx.org,2013:Post/15 2013-03-14T10:00:00-07:00 @@ -16,15 +25,15 @@ <img src="${static.url('images/press/releases/201x_240x180.jpg')}" /> <p></p> - - tag:www.edx.org,2013:Post/14 - 2013-02-20T10:00:00-07:00 - 2013-02-20T10:00:00-07:00 - - edX Expands Internationally and Doubles its Institutional Membership with the Addition of Six New Schools - <img src="${static.url('images/press/releases/edx-logo_240x180.png')}" /> - <p></p> - + + + + + + + + + tag:www.edx.org,2013:Post/14 2013-01-30T10:00:00-07:00 diff --git a/lms/templates/static_templates/press_releases/xblock_announcement.html b/lms/templates/static_templates/press_releases/xblock_announcement.html new file mode 100644 index 0000000000..e6deaae23c --- /dev/null +++ b/lms/templates/static_templates/press_releases/xblock_announcement.html @@ -0,0 +1,63 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">edX Takes First Step toward Open Source Vision by Releasing XBlock SDK +
+ + +
+
+

edX Takes First Step toward Open Source Vision by Releasing XBlock SDK

+
+
+

With Release of XBlock Source Code, Global Community Invited to Participate in the Development of the edX Learning Platform and the Next Generation of Online and Blended Courses

+ +

CAMBRIDGE, MA and SANTA CLARA, CA (PyCon 2013) – March 14, 2013 – EdX, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), today released its XBlock SDK to the general public under the Affero GPL open source license. XBlock is the underlying architecture supporting the rich, interactive course content found in edX courses. With XBlock, educational institutions are able to go far beyond simple text and videos to deliver interactive learning built specifically for the Internet environment. The release of the XBlock source code marks the first step toward edX’s vision of creating an open online learning platform that mirrors the collaborative philosophy of MOOCs themselves and is an invitation to the global community of developers to work with edX to deliver the world’s best and most accessible online learning experience.

+ +

XBlock is a component architecture that enables developers to create independent course components, or XBlocks, that are able to work seamlessly with other components in the construction and presentation of an online course. Course authors are able to combine XBlocks from a variety of sources – from text and video to sophisticated wiki-based collaborative learning environments and online laboratories – to create rich engaging online courses. The XBlock architecture will enable the easy integration of next generation education tools like the circuit simulator in edX’s popular Circuits and Electronics course (6.002x) and the molecular manipulator in the new Introduction to Biology – The Secret of Life course (7.00x) taught by Eric Lander, one of the leaders of the Human Genome Project.

+ +

XBlock is not limited to just delivering courses. A complete educational ecosystem will make use of a number of web applications, all of which require access to course content and data. XBlocks provide the structure and APIs needed to build components for use by those applications. edX will be working with independent developers to continue to extend the functionality of XBlock through the XBlock SDK and future open source initiatives.

+ +

“From its beginning, edX has been committed to developing the world’s best learning platform and tapping our global community to help us get there,” said Rob Rubin, edX Vice President of Engineering. “We look forward to working with the world’s developers, educators and researchers to help evolve the platform and ensure that everyone, everywhere has access to the world-class education that edX provides.”

+ +

The XBlock source code is available immediately and can be accessed at http://github.com/edX/XBlock.

+ +

About edX

+ +

EdX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.

+ +
+

Media Contacts:

+

Dan O'Connell

+

oconnell@edx.org

+

(617) 480-6585

+
+

Gerald Kimber White

+

gerald.kimberwhite@rfbinder.com

+

781-455-8250

+
+ + +
+
+
diff --git a/lms/urls.py b/lms/urls.py index 2c738913c3..ee213f2b8c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -172,11 +172,13 @@ urlpatterns = ('', url(r'^press/edx-expands-internationally$', 'static_template_view.views.render', {'template': 'press_releases/edx_expands_internationally.html'}, name="press/edx-expands-internationally"), - + url(r'^press/xblock_announcement$', 'static_template_view.views.render', + {'template': 'press_releases/xblock_announcement.html'}, + name="press/xblock-announcement"), # Should this always update to point to the latest press release? (r'^pressrelease$', 'django.views.generic.simple.redirect_to', - {'url': '/press/edx-expands-internationally'}), + {'url': '/press/xblock-announcement'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), From a4f10bfdafdfcc7e0619a9364e2706c18da1fd22 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Mar 2013 12:45:46 -0400 Subject: [PATCH 33/42] Small pylint cleanup --- cms/xmodule_namespace.py | 16 ++++++++++++++++ lms/xmodule_namespace.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index 391cac8eca..cad3110574 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -1,14 +1,27 @@ +""" +Namespace defining common fields used by Studio for all blocks +""" + import datetime from xblock.core import Namespace, Boolean, Scope, ModelType, String class StringyBoolean(Boolean): + """ + Reads strings from JSON as booleans. + + If the string is 'true' (case insensitive), then return True, + otherwise False. + + JSON values that aren't strings are returned as is + """ def from_json(self, value): if isinstance(value, basestring): return value.lower() == 'true' return value + class DateTuple(ModelType): """ ModelType that stores datetime objects as time tuples @@ -24,6 +37,9 @@ class DateTuple(ModelType): class CmsNamespace(Namespace): + """ + Namespace with fields common to all blocks in Studio + """ is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings) published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_by = String(help="Id of the user who published this module", scope=Scope.settings) diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index 4c04700a31..423c0eb0ec 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -1,14 +1,28 @@ -from xblock.core import Namespace, Boolean, Scope, String, List, Float +""" +Namespace that defines fields common to all blocks used in the LMS +""" +from xblock.core import Namespace, Boolean, Scope, String, Float from xmodule.fields import Date, Timedelta class StringyBoolean(Boolean): + """ + Reads strings from JSON as booleans. + + 'true' (case insensitive) return True, other strings return False + Other types are returned unchanged + """ def from_json(self, value): if isinstance(value, basestring): return value.lower() == 'true' return value + class StringyFloat(Float): + """ + Reads values as floats. If the value parses as a float, returns + that, otherwise returns None + """ def from_json(self, value): try: return float(value) @@ -17,6 +31,9 @@ class StringyFloat(Float): class LmsNamespace(Namespace): + """ + Namespace that defines fields common to all blocks used in the LMS + """ hide_from_toc = StringyBoolean( help="Whether to display this module in the table of contents", default=False, @@ -38,8 +55,14 @@ class LmsNamespace(Namespace): source_file = String(help="DO NOT USE", scope=Scope.settings) xqa_key = String(help="DO NOT USE", scope=Scope.settings) ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings) - graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) + graceperiod = Timedelta( + help="Amount of time after the due date that submissions will be accepted", + scope=Scope.settings + ) showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed") rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings) - days_early_for_beta = StringyFloat(help="Number of days early to show content to beta users", default=None, scope=Scope.settings) - + days_early_for_beta = StringyFloat( + help="Number of days early to show content to beta users", + default=None, + scope=Scope.settings + ) From 651465f1ad25c1c170b0daa6065d00df431a8dca Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 18 Mar 2013 12:56:17 -0400 Subject: [PATCH 34/42] add HTML templates for 404/500 errors --- cms/templates/404.html | 15 +++++++++++++++ cms/templates/500.html | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 cms/templates/404.html create mode 100644 cms/templates/500.html diff --git a/cms/templates/404.html b/cms/templates/404.html new file mode 100644 index 0000000000..abc7d9f30f --- /dev/null +++ b/cms/templates/404.html @@ -0,0 +1,15 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Page Not Found + +<%block name="content"> + +
+
+ +

Page not found

+

The page that you were looking for was not found. Go back to the homepage or let us know about any pages that may have been moved at technical@edx.org.

+
+
+ + \ No newline at end of file diff --git a/cms/templates/500.html b/cms/templates/500.html new file mode 100644 index 0000000000..f69ff35a49 --- /dev/null +++ b/cms/templates/500.html @@ -0,0 +1,14 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Server Error + +<%block name="content"> + +
+
+

Currently the edX servers are down

+

Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.

+
+
+ + \ No newline at end of file From 3fbde821dd26f99a818d751089308ddf41a17179 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Mar 2013 13:09:10 -0400 Subject: [PATCH 35/42] Skip over fields that are scoped to students when looping through the CourseModule fields in the instructor dashboard --- lms/djangoapps/instructor/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0be5724365..671283db9f 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -92,9 +92,15 @@ def instructor_dashboard(request, course_id): data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields: + if getattr(field.scope, 'student', False): + continue + data.append([field.name, json.dumps(field.read_json(course))]) for namespace in course.namespaces: for field in getattr(course, namespace).fields: + if getattr(field.scope, 'student', False): + continue + data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))]) datatable['data'] = data From 568f557dfc5663dd0a33cd80bae2b0f6abecf81c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 18 Mar 2013 13:50:25 -0400 Subject: [PATCH 36/42] Pep8 fixes Changed constant to uppercase --- common/djangoapps/terrain/browser.py | 3 +- common/djangoapps/terrain/factories.py | 2 +- common/djangoapps/terrain/steps.py | 1 + .../capa/capa/tests/response_xml_factory.py | 66 +++++++++++-------- lms/djangoapps/courseware/features/common.py | 10 ++- lms/djangoapps/courseware/features/courses.py | 1 + .../features/high-level-tabs.feature | 2 +- .../courseware/features/homepage.feature | 4 +- lms/djangoapps/courseware/features/login.py | 1 + .../courseware/features/openended.feature | 4 +- .../courseware/features/problems.feature | 2 +- .../courseware/features/problems.py | 53 ++++++++------- .../courseware/features/registration.py | 1 + .../features/smart-accordion.feature | 2 +- 14 files changed, 88 insertions(+), 64 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 6a60802d07..6394959532 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -6,7 +6,7 @@ import time # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches from lms import one_time_startup -from cms import one_time_startup +from cms import one_time_startup logger = getLogger(__name__) logger.info("Loading the lettuce acceptance testing terrain file...") @@ -21,6 +21,7 @@ def initial_setup(server): # world.browser = Browser('phantomjs') # world.browser = Browser('firefox') + @before.each_scenario def reset_data(scenario): # Clean out the django test database defined in the diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index 5ea34d1190..c36bf935f1 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -154,7 +154,7 @@ class XModuleItemFactory(Factory): # If a display name is set, use that dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex - dest_location = parent_location._replace(category=template.category, + dest_location = parent_location._replace(category=template.category, name=dest_name) new_item = store.clone_item(template, dest_location) diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 50fe0faf39..52eeb23c4a 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -101,6 +101,7 @@ def i_am_an_edx_user(step): #### helper functions + @world.absorb def scroll_to_bottom(): # Maximize the browser diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index 08ed1ca668..aa401b70cd 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -1,6 +1,7 @@ from lxml import etree from abc import ABCMeta, abstractmethod + class ResponseXMLFactory(object): """ Abstract base class for capa response XML factories. Subclasses override create_response_element and @@ -13,7 +14,7 @@ class ResponseXMLFactory(object): """ Subclasses override to return an etree element representing the capa response XML (e.g. ). - + The tree should NOT contain any input elements (such as ) as these will be added later.""" return None @@ -25,7 +26,7 @@ class ResponseXMLFactory(object): return None def build_xml(self, **kwargs): - """ Construct an XML string for a capa response + """ Construct an XML string for a capa response based on **kwargs. **kwargs is a dictionary that will be passed @@ -37,7 +38,7 @@ class ResponseXMLFactory(object): *question_text*: The text of the question to display, wrapped in

tags. - + *explanation_text*: The detailed explanation that will be shown if the user answers incorrectly. @@ -75,7 +76,7 @@ class ResponseXMLFactory(object): for i in range(0, int(num_responses)): response_element = self.create_response_element(**kwargs) root.append(response_element) - + # Add input elements for j in range(0, int(num_inputs)): input_element = self.create_input_element(**kwargs) @@ -135,7 +136,7 @@ class ResponseXMLFactory(object): # Names of group elements group_element_names = {'checkbox': 'checkboxgroup', 'radio': 'radiogroup', - 'multiple': 'choicegroup' } + 'multiple': 'choicegroup'} # Retrieve **kwargs choices = kwargs.get('choices', [True]) @@ -215,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): *answer*: Inline script that calculates the answer """ - + # Retrieve **kwargs cfn = kwargs.get('cfn', None) expect = kwargs.get('expect', None) @@ -245,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create the XML element. - + Uses *kwargs*: *answer*: The Python script used to evaluate the answer. @@ -272,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): For testing, we create a bare-bones version of .""" return etree.Element("schematic") + class CodeResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -284,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element: - + Uses **kwargs: - + *initial_display*: The code that initially appears in the textbox [DEFAULT: "Enter code here"] *answer_display*: The answer to display to the student @@ -326,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory): # return None here return None + class ChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ @@ -354,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): *num_samples*: The number of times to sample the student's answer to numerically compare it to the correct answer. - + *tolerance*: The tolerance within which answers will be accepted - [DEFAULT: 0.01] + [DEFAULT: 0.01] *answer*: The answer to the problem. Can be a formula string - or a Python variable defined in a script - (e.g. "$calculated_answer" for a Python variable + or a Python variable defined in a script + (e.g. "$calculated_answer" for a Python variable called calculated_answer) [REQUIRED] @@ -385,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # Set the sample information sample_str = self._sample_str(sample_dict, num_samples, tolerance) response_element.set("samples", sample_str) - + # Set the tolerance responseparam_element = etree.SubElement(response_element, "responseparam") @@ -406,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): # We could sample a different range, but for simplicity, # we use the same sample string for the hints - # that we used previously. + # that we used previously. formulahint_element.set("samples", sample_str) formulahint_element.set("answer", str(hint_prompt)) @@ -434,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): high_range_vals = [str(f[1]) for f in sample_dict.values()] sample_str = (",".join(sample_dict.keys()) + "@" + ",".join(low_range_vals) + ":" + - ",".join(high_range_vals) + + ",".join(high_range_vals) + "#" + str(num_samples)) return sample_str + class ImageResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -448,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): """ Create the element. - + Uses **kwargs: - + *src*: URL for the image file [DEFAULT: "/static/image.jpg"] *width*: Width of the image [DEFAULT: 100] @@ -488,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): input_element.set("src", str(src)) input_element.set("width", str(width)) input_element.set("height", str(height)) - + if rectangle: input_element.set("rectangle", rectangle) @@ -497,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): return input_element + class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -520,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): # Both display_src and display_class given, # or neither given - assert((display_src and display_class) or + assert((display_src and display_class) or (not display_src and not display_class)) # Create the element @@ -550,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): """ Create the element """ return etree.Element("javascriptinput") + class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -562,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class TrueFalseResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML """ @@ -574,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory): kwargs['choice_type'] = 'multiple' return ResponseXMLFactory.choicegroup_input_xml(**kwargs) + class OptionResponseXMLFactory(ResponseXMLFactory): """ Factory for producing XML""" @@ -618,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): """ Create a XML element. - + Uses **kwargs: *answer*: The correct answer (a string) [REQUIRED] @@ -640,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): # Create the element response_element = etree.Element("stringresponse") - # Set the answer attribute + # Set the answer attribute response_element.set("answer", str(answer)) # Set the case sensitivity @@ -665,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): def create_input_element(self, **kwargs): return ResponseXMLFactory.textline_input_xml(**kwargs) + class AnnotationResponseXMLFactory(ResponseXMLFactory): """ Factory for creating XML trees """ def create_response_element(self, **kwargs): @@ -677,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): input_element = etree.Element("annotationinput") text_children = [ - {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') }, - {'tag': 'text', 'text': kwargs.get('text', 'texty text') }, - {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') }, - {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') }, - {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') } + {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')}, + {'tag': 'text', 'text': kwargs.get('text', 'texty text')}, + {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah')}, + {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')}, + {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')} ] for child in text_children: etree.SubElement(input_element, child['tag']).text = child['text'] - default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')] + default_options = [('green', 'correct'),('eggs', 'incorrect'), ('ham', 'partially-correct')] options = kwargs.get('options', default_options) options_element = etree.SubElement(input_element, 'options') @@ -696,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): option_element.text = description return input_element - diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 4f307511df..8fb2843656 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -89,6 +89,7 @@ TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = "Problem" + @step(u'The course "([^"]*)" exists$') def create_course(step, course): @@ -100,7 +101,7 @@ def create_course(step, course): # Create the course # We always use the same org and display name, # but vary the course identifier (e.g. 600x or 191x) - course = CourseFactory.create(org=TEST_COURSE_ORG, + course = CourseFactory.create(org=TEST_COURSE_ORG, number=course, display_name=TEST_COURSE_NAME) @@ -112,6 +113,7 @@ def create_course(step, course): template='i4x://edx/templates/sequential/Empty', display_name=TEST_SECTION_NAME) + @step(u'I am registered for the course "([^"]*)"$') def i_am_registered_for_the_course(step, course): # Create the course @@ -126,6 +128,7 @@ def i_am_registered_for_the_course(step, course): world.log_in('robot@edx.org', 'test') + @step(u'The course "([^"]*)" has extra tab "([^"]*)"$') def add_tab_to_course(step, course, extra_tab_name): section_item = ItemFactory.create(parent_location=course_location(course), @@ -155,10 +158,12 @@ def flush_xmodule_store(): modulestore().collection.drop() update_templates() + def course_id(course_num): - return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, + return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, TEST_COURSE_NAME.replace(" ", "_")) + def course_location(course_num): return Location(loc_or_tag="i4x", org=TEST_COURSE_ORG, @@ -166,6 +171,7 @@ def course_location(course_num): category='course', name=TEST_COURSE_NAME.replace(" ", "_")) + def section_location(course_num): return Location(loc_or_tag="i4x", org=TEST_COURSE_ORG, diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py index eb5143b782..4fbbfd24f2 100644 --- a/lms/djangoapps/courseware/features/courses.py +++ b/lms/djangoapps/courseware/features/courses.py @@ -9,6 +9,7 @@ logger = getLogger(__name__) ## support functions + def get_courses(): ''' Returns dict of lists of courses available, keyed by course.org (ie university). diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature index 102f752e1f..931281a455 100644 --- a/lms/djangoapps/courseware/features/high-level-tabs.feature +++ b/lms/djangoapps/courseware/features/high-level-tabs.feature @@ -3,7 +3,7 @@ Feature: All the high level tabs should work As a student I want to navigate through the high level tabs -Scenario: I can navigate to all high-level tabs in a course +Scenario: I can navigate to all high -level tabs in a course Given: I am registered for the course "6.002x" And The course "6.002x" has extra tab "Custom Tab" And I log in diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature index 06a45c4bfa..c0c1c32f02 100644 --- a/lms/djangoapps/courseware/features/homepage.feature +++ b/lms/djangoapps/courseware/features/homepage.feature @@ -39,9 +39,9 @@ Feature: Homepage for web users | MITx | | HarvardX | | BerkeleyX | - | UTx | + | UTx | | WellesleyX | - | GeorgetownX | + | GeorgetownX | # # TODO: Add scenario that tests the courses available # # using a policy or a configuration file diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index ca7d710c61..094db078ca 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -34,6 +34,7 @@ def click_the_dropdown(step): #### helper functions + def user_is_an_unactivated_user(uname): u = User.objects.get(username=uname) u.is_active = False diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature index cc9f6e1c5f..1ab496144f 100644 --- a/lms/djangoapps/courseware/features/openended.feature +++ b/lms/djangoapps/courseware/features/openended.feature @@ -3,10 +3,10 @@ Feature: Open ended grading In order to complete the courseware questions I want the machine learning grading to be functional - # Commenting these all out right now until we can + # Commenting these all out right now until we can # make a reference implementation for a course with # an open ended grading problem that is always available - # + # # Scenario: An answer that is too short is rejected # Given I navigate to an openended question # And I enter the answer "z" diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 8828ebc699..a7fbac49c7 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -35,7 +35,7 @@ Feature: Answer choice problems Scenario: I can submit a blank answer Given I am viewing a "" problem - When I check a problem + When I check a problem Then My "" answer is marked "incorrect" Examples: diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 0b5ecbe20a..a6575c3d22 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -13,7 +13,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ # Factories from capa.tests.response_xml_factory that we will use # to generate the problem XML, with the keyword args used to configure # the output. -problem_factory_dict = { +PROBLEM_FACTORY_DICT = { 'drop down': { 'factory': OptionResponseXMLFactory(), 'kwargs': { @@ -32,8 +32,8 @@ problem_factory_dict = { 'factory': ChoiceResponseXMLFactory(), 'kwargs': { 'question_text': 'The correct answer is Choices 1 and 3', - 'choice_type':'checkbox', - 'choices':[True, False, True, False, False], + 'choice_type': 'checkbox', + 'choices': [True, False, True, False, False], 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}}, 'string': { @@ -41,7 +41,7 @@ problem_factory_dict = { 'kwargs': { 'question_text': 'The answer is "correct string"', 'case_sensitive': False, - 'answer': 'correct string' }}, + 'answer': 'correct string'}}, 'numerical': { 'factory': NumericalResponseXMLFactory(), @@ -49,13 +49,13 @@ problem_factory_dict = { 'question_text': 'The answer is pi + 1', 'answer': '4.14159', 'tolerance': '0.00001', - 'math_display': True }}, + 'math_display': True}}, 'formula': { 'factory': FormulaResponseXMLFactory(), 'kwargs': { 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]', - 'sample_dict': {'x': (-100, 100), 'y': (-100, 100) }, + 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)}, 'num_samples': 10, 'tolerance': 0.00001, 'math_display': True, @@ -77,15 +77,16 @@ problem_factory_dict = { a1=0 a2=0 return (a1+a2)==int(expect) - """) }}, + """)}}, } + def add_problem_to_course(course, problem_type): - assert(problem_type in problem_factory_dict) + assert(problem_type in PROBLEM_FACTORY_DICT) # Generate the problem XML using capa.tests.response_xml_factory - factory_dict = problem_factory_dict[problem_type] + factory_dict = PROBLEM_FACTORY_DICT[problem_type] problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) # Create a problem item using our generated XML @@ -95,7 +96,8 @@ def add_problem_to_course(course, problem_type): template="i4x://edx/templates/problem/Blank_Common_Problem", display_name=str(problem_type), data=problem_xml, - metadata={'rerandomize':'always'}) + metadata={'rerandomize': 'always'}) + @step(u'I am viewing a "([^"]*)" problem') def view_problem(step, problem_type): @@ -108,9 +110,9 @@ def view_problem(step, problem_type): # which should be loaded with the correct problem chapter_name = TEST_SECTION_NAME.replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % (chapter_name, section_name)) - + world.browser.visit(url) @@ -147,7 +149,7 @@ def answer_problem(step, problem_type, correctness): inputfield('string').fill(textvalue) elif problem_type == 'numerical': - textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2,2)) + textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) inputfield('numerical').fill(textvalue) elif problem_type == 'formula': @@ -170,14 +172,17 @@ def answer_problem(step, problem_type, correctness): # Submit the problem check_problem(step) + @step(u'I check a problem') def check_problem(step): world.browser.find_by_css("input.check").click() + @step(u'I reset the problem') def reset_problem(step): world.browser.find_by_css('input.reset').click() + @step(u'My "([^"]*)" answer is marked "([^"]*)"') def assert_answer_mark(step, problem_type, correctness): """ Assert that the expected answer mark is visible for a given problem type. @@ -190,28 +195,28 @@ def assert_answer_mark(step, problem_type, correctness): This can occur, for example, if the user has reset the problem. """ # Dictionaries that map problem types to the css selectors - # for correct/incorrect marks. + # for correct/incorrect marks. # The elements are lists of selectors because a particular problem type - # might be marked in multiple ways. - # For example, multiple choice is marked incorrect differently - # depending on whether the user selects an incorrect + # might be marked in multiple ways. + # For example, multiple choice is marked incorrect differently + # depending on whether the user selects an incorrect # item or submits without selecting any item) - correct_selectors = { 'drop down': ['span.correct'], + correct_selectors = {'drop down': ['span.correct'], 'multiple choice': ['label.choicegroup_correct'], 'checkbox': ['span.correct'], 'string': ['div.correct'], 'numerical': ['div.correct'], - 'formula': ['div.correct'], + 'formula': ['div.correct'], 'script': ['div.correct'], } - incorrect_selectors = { 'drop down': ['span.incorrect'], - 'multiple choice': ['label.choicegroup_incorrect', + incorrect_selectors = {'drop down': ['span.incorrect'], + 'multiple choice': ['label.choicegroup_incorrect', 'span.incorrect'], 'checkbox': ['span.incorrect'], 'string': ['div.incorrect'], 'numerical': ['div.incorrect'], - 'formula': ['div.incorrect'], - 'script': ['div.incorrect'] } + 'formula': ['div.incorrect'], + 'script': ['div.incorrect']} assert(correctness in ['correct', 'incorrect', 'unanswered']) assert(problem_type in correct_selectors and problem_type in incorrect_selectors) @@ -252,7 +257,7 @@ def inputfield(problem_type, choice=None, input_num=1): *choice* is the name of the checkbox input in a group of checkboxes. """ - sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % + sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % (problem_type.replace(" ", "_"), str(input_num))) if choice is not None: diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 9587842dd6..94b9b50f6c 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -2,6 +2,7 @@ from lettuce import world, step from lettuce.django import django_url from common import TEST_COURSE_ORG, TEST_COURSE_NAME + @step('I register for the course "([^"]*)"$') def i_register_for_the_course(step, course): cleaned_name = TEST_COURSE_NAME.replace(' ', '_') diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature index ccf1d45601..fc51eca25d 100644 --- a/lms/djangoapps/courseware/features/smart-accordion.feature +++ b/lms/djangoapps/courseware/features/smart-accordion.feature @@ -60,4 +60,4 @@ Feature: There are courses on the homepage # 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 + # Then I verify all the content of each course From 3e28ffa4f7ae43a46d1da533c9a836a8879540ac Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 18 Mar 2013 15:10:17 -0400 Subject: [PATCH 37/42] remove import and shortened line length --- cms/djangoapps/contentstore/views.py | 3 ++- cms/templates/404.html | 1 - cms/templates/500.html | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 80833a4e5f..eb634b0cdd 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -18,7 +18,8 @@ from django.core.files.temp import NamedTemporaryFile # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' from PIL import Image -from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError, HttpResponseNotFound +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError +from django.http import HttpResponseNotFound from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.core.context_processors import csrf diff --git a/cms/templates/404.html b/cms/templates/404.html index abc7d9f30f..a45a223bad 100644 --- a/cms/templates/404.html +++ b/cms/templates/404.html @@ -1,5 +1,4 @@ <%inherit file="base.html" /> -<%! from django.core.urlresolvers import reverse %> <%block name="title">Page Not Found <%block name="content"> diff --git a/cms/templates/500.html b/cms/templates/500.html index f69ff35a49..2645b0067b 100644 --- a/cms/templates/500.html +++ b/cms/templates/500.html @@ -1,5 +1,4 @@ <%inherit file="base.html" /> -<%! from django.core.urlresolvers import reverse %> <%block name="title">Server Error <%block name="content"> From 94d3ccf674a8333809d31fd2ca5c2462258ef28f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Mar 2013 10:41:12 -0400 Subject: [PATCH 38/42] Always compute the metadata inheritance tree, even on a course module, so that when walking down the tree, we always get the right data --- common/lib/xmodule/xmodule/modulestore/mongo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index c5e5bbfdf8..88a4933084 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -391,8 +391,7 @@ class MongoModuleStore(ModuleStoreBase): # if we are loading a course object, there is no parent to inherit the metadata from # so don't bother getting it - if item['location']['category'] != 'course': - metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location'])) + metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location'])) # TODO (cdodge): When the 'split module store' work has been completed, we should remove # the 'metadata_inheritance_tree' parameter From 0209c883bb8256acb86c320da306c1183c23a6d1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Mar 2013 10:41:38 -0400 Subject: [PATCH 39/42] Remove trailing whitespace --- common/lib/xmodule/xmodule/modulestore/mongo.py | 7 ++++--- lms/djangoapps/courseware/grades.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 88a4933084..95b74bf625 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -303,6 +303,7 @@ class MongoModuleStore(ModuleStoreBase): # this is likely a leaf node, so let's record what metadata we need to inherit metadata_to_inherit[child] = my_metadata + if root is not None: _compute_inherited_metadata(root) @@ -330,7 +331,7 @@ class MongoModuleStore(ModuleStoreBase): return tree def clear_cached_metadata_inheritance_tree(self, location): - key_name = '{0}/{1}'.format(location.org, location.course) + key_name = '{0}/{1}'.format(location.org, location.course) if self.metadata_inheritance_cache is not None: self.metadata_inheritance_cache.delete(key_name) @@ -611,7 +612,7 @@ class MongoModuleStore(ModuleStoreBase): self._update_single_item(location, {'metadata': metadata}) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) + self.get_cached_metadata_inheritance_tree(loc, force_refresh = True) def delete_item(self, location): """ @@ -631,7 +632,7 @@ class MongoModuleStore(ModuleStoreBase): self.collection.remove({'_id': Location(location).dict()}) # recompute (and update) the metadata inheritance tree which is cached - self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) + self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True) def get_parent_locations(self, location, course_id): diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index ecff14777d..e7f389696c 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -159,6 +159,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0% for moduledescriptor in section['xmoduledescriptors']: # Create a fake key to pull out a StudentModule object from the ModelDataCache + key = LmsKeyValueStore.Key( Scope.student_state, student.id, From 8fda5af4e4ec01fc0f4870e6b259963d6fa9dd91 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Mar 2013 10:54:46 -0400 Subject: [PATCH 40/42] Comment cleanup --- common/lib/xmodule/xmodule/modulestore/mongo.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 95b74bf625..aceebbf15f 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -388,10 +388,6 @@ class MongoModuleStore(ModuleStoreBase): resource_fs = OSFS(root) - metadata_inheritance_tree = None - - # if we are loading a course object, there is no parent to inherit the metadata from - # so don't bother getting it metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location'])) # TODO (cdodge): When the 'split module store' work has been completed, we should remove From 0884051b3241f9b3b4748ad732248b8e1cad4ba2 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 19 Mar 2013 19:29:09 +0200 Subject: [PATCH 41/42] Fixed bug where in firefox a marked poll button was not highlighted in blue. --- common/lib/xmodule/xmodule/css/poll/display.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/css/poll/display.scss b/common/lib/xmodule/xmodule/css/poll/display.scss index cfc03bcf91..82c018a3a0 100644 --- a/common/lib/xmodule/xmodule/css/poll/display.scss +++ b/common/lib/xmodule/xmodule/css/poll/display.scss @@ -131,6 +131,7 @@ section.poll_question { box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset; color: rgb(255, 255, 255); text-shadow: rgb(7, 103, 148) 0px 1px 0px; + background-image: none; } .text { From b5a587d8d0b8f7b68eb67ac4c58a0c14aaba3012 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 19 Mar 2013 16:57:19 -0400 Subject: [PATCH 42/42] Have to explicitly specify safe option on mongo insert, remove, update because the debug toolbar specifies a default of safe=False. --- cms/envs/dev.py | 5 ++--- common/lib/xmodule/xmodule/modulestore/mongo.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index f70f22512e..5612db1396 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = ( 'debug_toolbar.panels.sql.SQLDebugPanel', 'debug_toolbar.panels.signals.SignalDebugPanel', 'debug_toolbar.panels.logger.LoggingPanel', -# This is breaking Mongo updates-- Christina is investigating. -# 'debug_toolbar_mongo.panel.MongoDebugPanel', + 'debug_toolbar_mongo.panel.MongoDebugPanel', # Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and # Django=1.3.1/1.4 where requests to views get duplicated (your method gets @@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = { # To see stacktraces for MongoDB queries, set this to True. # Stacktraces slow down page loads drastically (for pages with lots of queries). -# DEBUG_TOOLBAR_MONGO_STACKTRACES = False +DEBUG_TOOLBAR_MONGO_STACKTRACES = False diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index aceebbf15f..1bf4763723 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -493,7 +493,10 @@ class MongoModuleStore(ModuleStoreBase): try: source_item = self.collection.find_one(location_to_query(source)) source_item['_id'] = Location(location).dict() - self.collection.insert(source_item) + self.collection.insert(source_item, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe) item = self._load_items([source_item])[0] # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so @@ -556,6 +559,9 @@ class MongoModuleStore(ModuleStoreBase): {'$set': update}, multi=False, upsert=True, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe ) if result['n'] == 0: raise ItemNotFoundError(location) @@ -626,7 +632,10 @@ class MongoModuleStore(ModuleStoreBase): course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name] self.update_metadata(course.location, own_metadata(course)) - self.collection.remove({'_id': Location(location).dict()}) + self.collection.remove({'_id': Location(location).dict()}, + # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False") + # from overriding our default value set in the init method. + safe=self.collection.safe) # recompute (and update) the metadata inheritance tree which is cached self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)