From 83d12cd7bd08470419ab154a91f504d0be02a126 Mon Sep 17 00:00:00 2001
From: Miles Steele
Date: Tue, 6 Aug 2013 14:38:44 -0400
Subject: [PATCH 01/92] add scroll to top on vertical view change
---
.../xmodule/js/src/sequence/display.coffee | 40 ++++++++++---------
1 file changed, 22 insertions(+), 18 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee
index 149a38e9ec8c..26246644ea00 100644
--- a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee
@@ -132,34 +132,38 @@ class @Sequence
else
alert 'Sequence error! Cannot navigate to tab ' + new_position + 'in the current SequenceModule. Please contact the course staff.'
- next: (event) =>
- event.preventDefault()
- new_position = @position + 1
- Logger.log "seq_next", old: @position, new: new_position, id: @id
-
- analytics.pageview @id
-
- # navigation using the next arrow
- analytics.track "Accessed Next Sequential",
- sequence_id: @id
- current_sequential: @position
- target_sequential: new_position
+ next: (event) => @_change_sequential 'seq_next', event
+ previous: (event) => @_change_sequential 'seq_prev', event
- @render new_position
+ # `direction` can be 'seq_prev' or 'seq_next'
+ _change_sequential: (direction, event) =>
+ # silently abort if direction is invalid.
+ return unless direction in ['seq_prev', 'seq_next']
- previous: (event) =>
event.preventDefault()
- new_position = @position - 1
- Logger.log "seq_prev", old: @position, new: new_position, id: @id
+ offset =
+ seq_next: 1
+ seq_prev: -1
+ new_position = @position + offset[direction]
+ Logger.log direction,
+ old: @position
+ new: new_position
+ id: @id
analytics.pageview @id
- # navigation using the previous arrow
- analytics.track "Accessed Previous Sequential",
+ # navigation using the next or previous arrow button.
+ tracking_messages =
+ seq_prev: "Accessed Previous Sequential"
+ seq_next: "Accessed Next Sequential"
+ analytics.track tracking_messages[direction],
sequence_id: @id
current_sequential: @position
target_sequential: new_position
+ # If the bottom nav is used, scroll to the top of the page on change.
+ if $(event.target).closest('nav[class="sequence-bottom"]').length > 0
+ $.scrollTo 0, 150
@render new_position
link_for: (position) ->
From 84322694cd269ac7c03e192a4c833accec3b0e87 Mon Sep 17 00:00:00 2001
From: Miles Steele
Date: Thu, 8 Aug 2013 11:42:29 -0400
Subject: [PATCH 02/92] add jasmine test for scroll to top
---
.../xmodule/xmodule/js/spec/sequence/display_spec.coffee | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.coffee
index 1944f7dc74ba..7d7b51c416f7 100644
--- a/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.coffee
@@ -134,6 +134,7 @@ xdescribe 'Sequence', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 2
+ $.scrollTo 150
$('.sequence-nav-buttons .next a').click()
it 'log the next sequence event', ->
@@ -142,10 +143,14 @@ xdescribe 'Sequence', ->
it 'call render on the next sequence', ->
expect($('#seq_content').html()).toEqual 'Sample Problem'
+ it 'scrolls to the top of the page', ->
+ expect($('body').scrollTop()).toBe 0
+
describe 'previous', ->
beforeEach ->
jasmine.stubRequests()
@sequence = new Sequence '1', 'sequence_1', @items, 'sequence', 2
+ $.scrollTo 150
$('.sequence-nav-buttons .prev a').click()
it 'log the previous sequence event', ->
@@ -154,6 +159,9 @@ xdescribe 'Sequence', ->
it 'call render on the previous sequence', ->
expect($('#seq_content').html()).toEqual 'Video 1'
+ it 'scrolls to the top of the page', ->
+ expect($('body').scrollTop()).toBe 0
+
describe 'link_for', ->
it 'return a link for specific position', ->
sequence = new Sequence '1', 'sequence_1', @items, 2
From cdf0eef5ca2b028cf4cff6eb0e4543be26b7f34d Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Mon, 16 Sep 2013 15:06:51 -0400
Subject: [PATCH 03/92] Consider redirect code as succcesful
---
cms/templates/import.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/templates/import.html b/cms/templates/import.html
index 27337bf235d3..88ccfd4e7746 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -77,7 +77,7 @@
${_("Course to import:")}
e.preventDefault();
submitBtn.hide();
data.submit().complete(function(result, textStatus, xhr) {
- if (result.status != 200) {
+ if (result.status != 200 && result.status != 302) {
alert('${_("Your import has failed.")}\n\n' + JSON.parse(result.responseText)["ErrMsg"]);
submitBtn.show();
bar.hide();
From e726ef4c46f6fbcc4ea0c95367fe01dea8744d15 Mon Sep 17 00:00:00 2001
From: James Tauber
Date: Mon, 16 Sep 2013 16:30:26 -0400
Subject: [PATCH 04/92] Link to How To Contribute wiki page
Created a CONTRIBUTING.rst which GitHub will pick up and included a
link to the How To Contribute wiki page there. Also added the same link
to the README in the "How To Contribute" section.
---
CONTRIBUTING.rst | 6 ++++++
README.md | 6 +++---
2 files changed, 9 insertions(+), 3 deletions(-)
create mode 100644 CONTRIBUTING.rst
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 000000000000..146e4f09fc14
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,6 @@
+How To Contribute
+=================
+
+Contributions are very welcome.
+
+Please read `How To Contribute `_ for details.
diff --git a/README.md b/README.md
index 0261f87b46b0..a595e306c2fb 100644
--- a/README.md
+++ b/README.md
@@ -345,9 +345,9 @@ with `overview.md` to get an introduction to the architecture of the system.
How to Contribute
-----------------
-Contributions are very welcome. The easiest way is to fork this repo, and then
-make a pull request from your fork. The first time you make a pull request, you
-may be asked to sign a Contributor Agreement.
+Contributions are very welcome.
+
+Please read [How To Contribute](https://github.com/edx/edx-platform/wiki/How-To-Contribute) for details.
Reporting Security Issues
-------------------------
From ca1f4bad7d6d265be7bf8e7d45c5c617d8e6d44f Mon Sep 17 00:00:00 2001
From: James Tauber
Date: Mon, 16 Sep 2013 16:50:08 -0400
Subject: [PATCH 05/92] Moved to a more comprehensive contributing doc.
Based on the How To Contribute wiki page (and switched to markdown so
it's a simple copy-paste).
---
CONTRIBUTING.md | 13 +++++++++++++
CONTRIBUTING.rst | 6 ------
2 files changed, 13 insertions(+), 6 deletions(-)
create mode 100644 CONTRIBUTING.md
delete mode 100644 CONTRIBUTING.rst
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000000..24dcf4673c6b
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+Contributions are very welcome. The easiest way is to fork the repo and then make a pull request from your fork. Before your pull request is merged, it will be reviewed by at least one person. There may be feedback so expect comments on the pull request. Add yourself to the AUTHORS file in your first pull request.
+
+Please review:
+
+* [[Python Guidelines]]
+* [[Javascript Guidelines]]
+* [Testing](https://github.com/edx/edx-platform/blob/master/docs/internal/testing.md)
+
+Coding conventions should be followed and your commit should *increase* test coverage, not decrease it. For more involved contributions, you may want to discuss your intentions on the mailing list *before* you start coding.
+
+Before your first pull request is merged, you'll need to sign the [individual contributor agreement](http://code.edx.org/individual-contributor-agreement.pdf) and send it in. This confirms you have the authority to contribute the code in the pull request and ensures we can relicense it.
+
+If you have any questions, please ask on the [mailing list](https://groups.google.com/forum/#!forum/edx-code).
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
deleted file mode 100644
index 146e4f09fc14..000000000000
--- a/CONTRIBUTING.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-How To Contribute
-=================
-
-Contributions are very welcome.
-
-Please read `How To Contribute `_ for details.
From 9b65a1a90d61ebe7af9ae0eb603d798a413e9a6f Mon Sep 17 00:00:00 2001
From: James Tauber
Date: Mon, 16 Sep 2013 17:04:38 -0400
Subject: [PATCH 06/92] Link to wiki with absolute URLs
---
CONTRIBUTING.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 24dcf4673c6b..9bfd04bb935d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,8 +2,8 @@ Contributions are very welcome. The easiest way is to fork the repo and then mak
Please review:
-* [[Python Guidelines]]
-* [[Javascript Guidelines]]
+* [Python Guidelines](https://github.com/edx/edx-platform/wiki/Python-Guidelines)
+* [Javascript Guidelines](https://github.com/edx/edx-platform/wiki/Javascript-Guidelines)
* [Testing](https://github.com/edx/edx-platform/blob/master/docs/internal/testing.md)
Coding conventions should be followed and your commit should *increase* test coverage, not decrease it. For more involved contributions, you may want to discuss your intentions on the mailing list *before* you start coding.
From a1693dced7260d366940c21a1e973d53fbec0bcb Mon Sep 17 00:00:00 2001
From: James Tauber
Date: Mon, 16 Sep 2013 17:05:57 -0400
Subject: [PATCH 07/92] Added linebreaks.
---
CONTRIBUTING.md | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9bfd04bb935d..d2cfdfdf9357 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,4 +1,7 @@
-Contributions are very welcome. The easiest way is to fork the repo and then make a pull request from your fork. Before your pull request is merged, it will be reviewed by at least one person. There may be feedback so expect comments on the pull request. Add yourself to the AUTHORS file in your first pull request.
+Contributions are very welcome. The easiest way is to fork the repo and then
+make a pull request from your fork. Before your pull request is merged, it will
+be reviewed by at least one person. There may be feedback so expect comments on
+the pull request. Add yourself to the AUTHORS file in your first pull request.
Please review:
@@ -6,8 +9,14 @@ Please review:
* [Javascript Guidelines](https://github.com/edx/edx-platform/wiki/Javascript-Guidelines)
* [Testing](https://github.com/edx/edx-platform/blob/master/docs/internal/testing.md)
-Coding conventions should be followed and your commit should *increase* test coverage, not decrease it. For more involved contributions, you may want to discuss your intentions on the mailing list *before* you start coding.
+Coding conventions should be followed and your commit should *increase* test
+coverage, not decrease it. For more involved contributions, you may want to
+discuss your intentions on the mailing list *before* you start coding.
-Before your first pull request is merged, you'll need to sign the [individual contributor agreement](http://code.edx.org/individual-contributor-agreement.pdf) and send it in. This confirms you have the authority to contribute the code in the pull request and ensures we can relicense it.
+Before your first pull request is merged, you'll need to sign the
+[individual contributor agreement](http://code.edx.org/individual-contributor-agreement.pdf)
+and send it in. This confirms you have the authority to contribute the code in
+the pull request and ensures we can relicense it.
-If you have any questions, please ask on the [mailing list](https://groups.google.com/forum/#!forum/edx-code).
+If you have any questions, please ask on the
+[mailing list](https://groups.google.com/forum/#!forum/edx-code).
From 0f81bdcefd7d10164949e4607063326a7ba38162 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Mon, 16 Sep 2013 17:25:41 -0400
Subject: [PATCH 08/92] Properly handle error responses
---
cms/templates/import.html | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/cms/templates/import.html b/cms/templates/import.html
index 88ccfd4e7746..b3caac54a338 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -77,16 +77,11 @@
},
done: function(e, data){
bar.hide();
+ window.onbeforeunload = null;
alert('${_("Your import was successful.")}');
window.location = '${successful_import_redirect_url}';
},
+ start: function(e) {
+ window.onbeforeunload = function() {
+ return "Your import is in progress; navigating " +
+ "away will abort it.";
+ }
+ },
sequentialUploads: true
-
-
});
})();
From d8e9ffbb0b736c8b782d3fe72e2104fd2d37f8be Mon Sep 17 00:00:00 2001
From: Usman Khalid
Date: Thu, 19 Sep 2013 12:38:32 +0000
Subject: [PATCH 10/92] Added missing test in StaticImageBookTest
---
lms/djangoapps/staticbook/tests.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py
index 135150a2d1d2..143f5a46ab30 100644
--- a/lms/djangoapps/staticbook/tests.py
+++ b/lms/djangoapps/staticbook/tests.py
@@ -105,6 +105,13 @@ def test_out_of_range_book_id(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
+ def test_page_xss(self):
+ # The page in the URL used to go right on the page.
+ self.make_course(textbooks=[IMAGE_BOOK])
+ # It's no longer possible to use a non-integer page.
+ with self.assertRaises(NoReverseMatch):
+ self.make_url('book', book_index=0, page='xyzzy')
+
class StaticPdfBookTest(StaticBookTest):
"""
From 0d0cb5650179384ef155dfa7dba419fa9b0256d1 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 19 Sep 2013 12:13:01 -0400
Subject: [PATCH 11/92] Speed up Location creation returning existing locations
where possible
---
common/lib/xmodule/xmodule/modulestore/__init__.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index e53b4adb5e8b..87d492c9bd89 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -169,7 +169,9 @@ def check(val, regexp):
# names allow colons
check(list_[4], INVALID_CHARS_NAME)
- if isinstance(location, basestring):
+ if isinstance(location, Location):
+ return location
+ elif isinstance(location, basestring):
match = URL_RE.match(location)
if match is None:
log.debug('location is instance of %s but no URL match' % basestring)
@@ -195,8 +197,6 @@ def check(val, regexp):
check_dict(kwargs)
return _LocationBase.__new__(_cls, **kwargs)
- elif isinstance(location, Location):
- return _LocationBase.__new__(_cls, location)
else:
raise InvalidLocationError(location)
From 2c90dc73cdce480cea00f61bac26fb36b0456410 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 16 Sep 2013 13:12:16 -0400
Subject: [PATCH 12/92] Don't access kvs directly but use x_module and
field_data mechanisms
---
common/lib/xmodule/xmodule/modulestore/inheritance.py | 3 +--
common/lib/xmodule/xmodule/modulestore/mongo/base.py | 9 +++------
common/lib/xmodule/xmodule/modulestore/mongo/draft.py | 5 +++--
common/lib/xmodule/xmodule/x_module.py | 4 ++--
4 files changed, 9 insertions(+), 12 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
index 78d584580fd3..d68c121b96ba 100644
--- a/common/lib/xmodule/xmodule/modulestore/inheritance.py
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -56,8 +56,7 @@ def compute_inherited_metadata(descriptor):
parent_metadata = descriptor.xblock_kvs.inherited_settings.copy()
# add any of descriptor's explicitly set fields to the inheriting list
for field in InheritanceMixin.fields.values():
- # pylint: disable = W0212
- if descriptor._field_data.has(descriptor, field.name):
+ if field.is_set_on(descriptor):
# inherited_settings values are json repr
parent_metadata[field.name] = field.read_json(descriptor)
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py
index f4c88387f424..44865ab58e44 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py
@@ -324,16 +324,14 @@ def compute_metadata_inheritance_tree(self, location):
for result in resultset:
location = Location(result['_id'])
# We need to collate between draft and non-draft
- # i.e. draft verticals can have children which are not in non-draft versions
+ # i.e. draft verticals will have draft children but will have non-draft parents currently
location = location.replace(revision=None)
location_url = location.url()
if location_url in results_by_url:
existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
additional_children = result.get('definition', {}).get('children', [])
total_children = existing_children + additional_children
- if 'definition' not in results_by_url[location_url]:
- results_by_url[location_url]['definition'] = {}
- results_by_url[location_url]['definition']['children'] = total_children
+ results_by_url[location_url].setdefault('definition', {})['children'] = total_children
results_by_url[location.url()] = result
if location.category == 'course':
root = location.url()
@@ -643,12 +641,11 @@ def save_xmodule(self, xmodule):
"""
# Save any changes to the xmodule to the MongoKeyValueStore
xmodule.save()
- # split mongo's persist_dag is more general and useful.
self.collection.save({
'_id': xmodule.location.dict(),
'metadata': own_metadata(xmodule),
'definition': {
- 'data': xmodule.xblock_kvs._data,
+ 'data': xmodule.get_explicitly_set_fields_by_scope(Scope.content),
'children': xmodule.children if xmodule.has_children else []
}
})
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
index bed42b30a909..0f46d3fe0c91 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
@@ -15,6 +15,7 @@
from xmodule.modulestore.mongo.base import location_to_query, namedtuple_to_son, get_course_id_no_run, MongoModuleStore
import pymongo
from pytz import UTC
+from xblock.fields import Scope
DRAFT = 'draft'
# Things w/ these categories should never be marked as version='draft'
@@ -237,8 +238,8 @@ def publish(self, location, published_by_id):
draft.published_date = datetime.now(UTC)
draft.published_by = published_by_id
- super(DraftModuleStore, self).update_item(location, draft._field_data._kvs._data)
- super(DraftModuleStore, self).update_children(location, draft._field_data._kvs._children)
+ super(DraftModuleStore, self).update_item(location, draft.get_explicitly_set_fields_by_scope(Scope.content))
+ super(DraftModuleStore, self).update_children(location, draft.children)
super(DraftModuleStore, self).update_metadata(location, own_metadata(draft))
self.delete_item(location)
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index e773c9cc5411..3b1254e77516 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -670,8 +670,8 @@ def get_explicitly_set_fields_by_scope(self, scope=Scope.content):
"""
result = {}
for field in self.fields.values():
- if (field.scope == scope and self._field_data.has(self, field.name)):
- result[field.name] = self._field_data.get(self, field.name)
+ if (field.scope == scope and field.is_set_on(self)):
+ result[field.name] = field.read_json(self)
return result
@property
From b9a7c7f752607ace786044cd3de4e4403f3f4b02 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 16 Sep 2013 13:27:29 -0400
Subject: [PATCH 13/92] Create new structure allows override of usage id and
category
---
.../xmodule/modulestore/split_mongo/split.py | 17 +++++++-------
.../tests/test_split_modulestore.py | 23 ++++++++++++++++++-
2 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index bb9aa59016e2..17976abe7879 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -378,7 +378,7 @@ def get_instance(self, course_id, location, depth=0):
"""
return self.get_item(location, depth=depth)
- def get_parent_locations(self, locator, usage_id=None):
+ def get_parent_locations(self, locator, course_id=None):
'''
Return the locations (Locators w/ usage_ids) for the parents of this location in this
course. Could use get_items(location, {'children': usage_id}) but this is slightly faster.
@@ -387,7 +387,6 @@ def get_parent_locations(self, locator, usage_id=None):
:param locator: BlockUsageLocator restricting search scope
:param usage_id: ignored. Only included for API compatibility. Specify the usage_id within the locator.
'''
-
course = self._lookup_course(locator)
items = []
for parent_id, value in course['blocks'].iteritems():
@@ -717,7 +716,9 @@ def create_item(self, course_or_parent_locator, category, user_id, definition_lo
def create_course(
self, org, prettyid, user_id, id_root=None, fields=None,
- master_branch='draft', versions_dict=None, root_category='course'):
+ master_branch='draft', versions_dict=None, root_category='course',
+ root_usage_id='course'
+ ):
"""
Create a new entry in the active courses index which points to an existing or new structure. Returns
the course root of the resulting entry (the location has the course id)
@@ -749,7 +750,7 @@ def create_course(
provide any fields overrides, see above). if not provided, will create a mostly empty course
structure with just a category course root xblock.
"""
- partitioned_fields = self._partition_fields_by_scope('course', fields)
+ partitioned_fields = self._partition_fields_by_scope(root_category, fields)
block_fields = partitioned_fields.setdefault(Scope.settings, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
@@ -773,13 +774,13 @@ def create_course(
self.definitions.update({'_id': definition_id}, {'$set': {"edit_info.original_version": definition_id}})
draft_structure = {
- 'root': 'course',
+ 'root': root_usage_id,
'previous_version': None,
'edited_by': user_id,
'edited_on': datetime.datetime.now(UTC),
'blocks': {
- 'course': {
- 'category': 'course',
+ root_usage_id: {
+ 'category': root_category,
'definition': definition_id,
'fields': block_fields,
'edit_info': {
@@ -794,7 +795,7 @@ def create_course(
draft_structure['original_version'] = new_id
self.structures.update({'_id': new_id},
{'$set': {"original_version": new_id,
- 'blocks.course.edit_info.update_version': new_id}})
+ 'blocks.{}.edit_info.update_version'.format(root_usage_id): new_id}})
if versions_dict is None:
versions_dict = {master_branch: new_id}
else:
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
index b299f56711e2..832fd2091e5f 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
@@ -17,6 +17,7 @@
from pytz import UTC
from path import path
import re
+import random
class SplitModuleTest(unittest.TestCase):
@@ -250,7 +251,6 @@ def test_course_successors(self):
self.assertEqual(str(result.children[0].locator.version_guid), self.GUID_D1)
self.assertEqual(len(result.children[0].children), 1)
-
class SplitModuleItemTests(SplitModuleTest):
'''
Item read tests including inheritance
@@ -967,6 +967,27 @@ def test_update_course_index(self):
course = modulestore().get_course(CourseLocator(course_id=locator.course_id, branch="published"))
self.assertEqual(str(course.location.version_guid), self.GUID_D1)
+ def test_create_with_root(self):
+ """
+ Test create_course with a specified root id and category
+ """
+ user = random.getrandbits(32)
+ new_course = modulestore().create_course(
+ 'test_org', 'test_transaction', user,
+ root_usage_id='top', root_category='chapter'
+ )
+ self.assertEqual(new_course.location.usage_id, 'top')
+ self.assertEqual(new_course.category, 'chapter')
+ # look at db to verify
+ db_structure = modulestore().structures.find_one({
+ '_id': new_course.location.as_object_id(new_course.location.version_guid)
+ })
+ self.assertIsNotNone(db_structure, "Didn't find course")
+ self.assertNotIn('course', db_structure['blocks'])
+ self.assertIn('top', db_structure['blocks'])
+ self.assertEqual(db_structure['blocks']['top']['category'], 'chapter')
+
+
class TestInheritance(SplitModuleTest):
"""
From bf633f9c5d298ce8435999c2f9d96c8a216ff12a Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Fri, 20 Sep 2013 10:26:15 -0400
Subject: [PATCH 14/92] Perform auth checks in middleware
Includes static server tests.
---
common/djangoapps/contentserver/middleware.py | 14 ++-
.../contentserver/tests/__init__.py | 0
common/djangoapps/contentserver/tests/test.py | 118 ++++++++++++++++++
lms/envs/test.py | 7 ++
4 files changed, 138 insertions(+), 1 deletion(-)
create mode 100644 common/djangoapps/contentserver/tests/__init__.py
create mode 100644 common/djangoapps/contentserver/tests/test.py
diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py
index d89e3fdd2331..0174c39fea54 100644
--- a/common/djangoapps/contentserver/middleware.py
+++ b/common/djangoapps/contentserver/middleware.py
@@ -1,4 +1,6 @@
from django.http import HttpResponse, HttpResponseNotModified
+from django.shortcuts import redirect
+from student.models import CourseEnrollment
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
@@ -20,7 +22,8 @@ def process_request(self, request):
return response
# first look in our cache so we don't have to round-trip to the DB
- content = get_cached_content(loc)
+ #content = get_cached_content(loc)
+ content = None
if content is None:
# nope, not in cache, let's fetch from DB
try:
@@ -41,6 +44,15 @@ def process_request(self, request):
# NOP here, but we may wish to add a "cache-hit" counter in the future
pass
+ # Check that user has access to content
+ if getattr(content, "locked", False):
+ if not hasattr(request, "user") or not request.user.is_authenticated():
+ return redirect('root')
+ course_id = "/".join([loc.org, loc.course, loc.name])
+ if not CourseEnrollment.is_enrolled(request.user, course_id):
+ return redirect('dashboard')
+
+
# see if the last-modified at hasn't changed, if not return a 302 (Not Modified)
# convert over the DB persistent last modified timestamp to a HTTP compatible
diff --git a/common/djangoapps/contentserver/tests/__init__.py b/common/djangoapps/contentserver/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/common/djangoapps/contentserver/tests/test.py b/common/djangoapps/contentserver/tests/test.py
new file mode 100644
index 000000000000..d084494be397
--- /dev/null
+++ b/common/djangoapps/contentserver/tests/test.py
@@ -0,0 +1,118 @@
+"""
+Tests for StaticContentServer
+"""
+import copy
+import logging
+from uuid import uuid4
+from path import path
+from pymongo import MongoClient
+
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.test.client import Client
+from django.test.utils import override_settings
+
+from student.models import CourseEnrollment
+
+from xmodule.contentstore.django import contentstore, _CONTENTSTORE
+from xmodule.modulestore import Location
+from xmodule.contentstore.content import StaticContent
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.tests.django_utils import (studio_store_config,
+ ModuleStoreTestCase)
+from xmodule.modulestore.xml_importer import import_from_xml
+
+log = logging.getLogger(__name__)
+
+TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
+TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
+
+TEST_MODULESTORE = studio_store_config(settings.TEST_ROOT / "data")
+
+
+@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
+class ContentStoreToyCourseTest(ModuleStoreTestCase):
+ """
+ Tests that use the toy course.
+ """
+
+ def setUp(self):
+ """
+ Create user and login.
+ """
+
+ settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
+ settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+
+ self.client = Client()
+
+ loc = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt' )
+ self.loc = loc
+
+ rel_url = StaticContent.get_url_path_from_location(loc)
+ base = "http://127.0.0.1:8000"
+
+ self.contentstore = contentstore()
+ import_from_xml(modulestore('direct'), 'common/test/data/', ['toy'],
+ static_content_store=self.contentstore, verbose=True)
+ self.url = base + rel_url
+
+ def tearDown(self):
+
+ MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
+ _CONTENTSTORE.clear()
+
+ def test_aunlocked_asset(self):
+ """
+ Test that unlocked assets are being served.
+ """
+ # Unlock the asset
+ self.contentstore.set_attr(self.loc, 'locked', False)
+ resp = self.client.get(self.url)
+ self.assertEqual(resp.status_code, 200)
+
+
+ def test_locked_asset(self):
+ """
+ Test that locked assets behave appropriately in case:
+ (1) User is not logged in
+ (2) User is logged in in but not registerd for the course
+ (3) User is logged in and registered
+ """
+
+ # Lock the asset
+ self.contentstore.set_attr(self.loc, 'locked', True)
+
+
+ # Case (1)
+ resp = self.client.get(self.url)
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(resp.has_header("LOCATION"))
+
+ # Case (2)
+ # Create user and login
+ uname = 'testuser'
+ email = 'test+courses@edx.org'
+ password = 'foo'
+ user = User.objects.create_user(uname, email, password)
+ user.is_active = True
+ user.save()
+ self.client.login(username=uname, password=password)
+ log.debug("User logged in")
+
+ resp = self.client.get(self.url)
+ log.debug("Received response %s", resp)
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(resp.has_header("LOCATION"))
+ self.assertIn("dashboard", resp["LOCATION"])
+
+ # Case (3)
+ # Enroll student
+ course_id = "/".join([self.loc.org, self.loc.course, self.loc.name])
+ self.assertTrue(CourseEnrollment.enroll(user, course_id))
+ self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
+
+ resp = self.client.get(self.url)
+ self.assertEqual(resp.status_code, 200)
+
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 94feffdf3ed1..45e934ec1f21 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -97,6 +97,13 @@
}
}
+CONTENTSTORE = {
+ 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
+ 'OPTIONS': {
+ 'host': 'localhost',
+ 'db': 'xcontent',
+ }
+}
DATABASES = {
'default': {
From 7a8d463260d96900c1c11e8e428a3b960077c9ce Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Fri, 20 Sep 2013 17:22:51 -0400
Subject: [PATCH 15/92] Put static server middleware after the middleware it
requires
---
lms/envs/common.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 81b01179d7ca..466ec262f909 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -542,7 +542,6 @@
)
MIDDLEWARE_CLASSES = (
- 'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django_comment_client.middleware.AjaxExceptionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -551,6 +550,7 @@
# Instead of AuthenticationMiddleware, we use a cached backed version
#'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
+ 'contentserver.middleware.StaticContentServer',
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
From 268b5b47aeca7315650c42dafcff086bf03904eb Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Fri, 20 Sep 2013 17:25:43 -0400
Subject: [PATCH 16/92] Remove cache workaround
---
common/djangoapps/contentserver/middleware.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py
index 0174c39fea54..8d81929693cb 100644
--- a/common/djangoapps/contentserver/middleware.py
+++ b/common/djangoapps/contentserver/middleware.py
@@ -22,8 +22,7 @@ def process_request(self, request):
return response
# first look in our cache so we don't have to round-trip to the DB
- #content = get_cached_content(loc)
- content = None
+ content = get_cached_content(loc)
if content is None:
# nope, not in cache, let's fetch from DB
try:
@@ -47,7 +46,7 @@ def process_request(self, request):
# Check that user has access to content
if getattr(content, "locked", False):
if not hasattr(request, "user") or not request.user.is_authenticated():
- return redirect('root')
+ return redirect('login')
course_id = "/".join([loc.org, loc.course, loc.name])
if not CourseEnrollment.is_enrolled(request.user, course_id):
return redirect('dashboard')
From 3c3edac05fa6720075662ce9a672e4c3704faf76 Mon Sep 17 00:00:00 2001
From: Usman Khalid
Date: Mon, 23 Sep 2013 12:58:14 +0000
Subject: [PATCH 17/92] Fixed tests name in staticbook
---
lms/djangoapps/staticbook/tests.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/lms/djangoapps/staticbook/tests.py b/lms/djangoapps/staticbook/tests.py
index 143f5a46ab30..999dfcb8cf31 100644
--- a/lms/djangoapps/staticbook/tests.py
+++ b/lms/djangoapps/staticbook/tests.py
@@ -105,10 +105,9 @@ def test_out_of_range_book_id(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
- def test_page_xss(self):
- # The page in the URL used to go right on the page.
+ def test_bad_page_id(self):
+ # A bad page id will cause a 404.
self.make_course(textbooks=[IMAGE_BOOK])
- # It's no longer possible to use a non-integer page.
with self.assertRaises(NoReverseMatch):
self.make_url('book', book_index=0, page='xyzzy')
From c196d1895abc583b811ad79d2e67fc92f3e555c4 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Mon, 23 Sep 2013 10:13:33 -0400
Subject: [PATCH 18/92] i18n fix
---
cms/templates/import.html | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/cms/templates/import.html b/cms/templates/import.html
index 706c56351ab4..2973d6ef4f63 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -104,8 +104,7 @@
${_("Course to import:")}
},
start: function(e) {
window.onbeforeunload = function() {
- return "Your import is in progress; navigating " +
- "away will abort it.";
+ return '${_("Your import is in progress; navigating away will abort it.")}';
}
},
sequentialUploads: true
From b4e6a8b209e45b641a112e569684f57049c6131a Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Mon, 23 Sep 2013 10:52:05 -0400
Subject: [PATCH 19/92] Fix order-dependency in tests
---
common/djangoapps/contentserver/tests/test.py | 35 +++++++++++--------
.../test/data/toy/static/another_static.txt | 17 +++++++++
2 files changed, 38 insertions(+), 14 deletions(-)
create mode 100644 common/test/data/toy/static/another_static.txt
diff --git a/common/djangoapps/contentserver/tests/test.py b/common/djangoapps/contentserver/tests/test.py
index d084494be397..340092b4d346 100644
--- a/common/djangoapps/contentserver/tests/test.py
+++ b/common/djangoapps/contentserver/tests/test.py
@@ -45,31 +45,42 @@ def setUp(self):
settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+ base = "http://127.0.0.1:8000"
self.client = Client()
+ self.contentstore = contentstore()
+ # A locked the asset
loc = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt' )
- self.loc = loc
-
+ self.loc = loc
rel_url = StaticContent.get_url_path_from_location(loc)
- base = "http://127.0.0.1:8000"
+ self.url = base + rel_url
+
+ # An unlocked asset
+ loc2 = Location('c4x', 'edX', 'toy', 'asset', 'another_static.txt' )
+ self.loc2 = loc2
+ rel_url2 = StaticContent.get_url_path_from_location(loc2)
+ self.url2 = base + rel_url2
+
- self.contentstore = contentstore()
import_from_xml(modulestore('direct'), 'common/test/data/', ['toy'],
static_content_store=self.contentstore, verbose=True)
- self.url = base + rel_url
+
+ self.contentstore.set_attr(self.loc, 'locked', True)
+
def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
- def test_aunlocked_asset(self):
+ def test_unlocked_asset(self):
"""
Test that unlocked assets are being served.
"""
- # Unlock the asset
- self.contentstore.set_attr(self.loc, 'locked', False)
- resp = self.client.get(self.url)
+ # Logout user
+ self.client.logout()
+
+ resp = self.client.get(self.url2)
self.assertEqual(resp.status_code, 200)
@@ -81,10 +92,6 @@ def test_locked_asset(self):
(3) User is logged in and registered
"""
- # Lock the asset
- self.contentstore.set_attr(self.loc, 'locked', True)
-
-
# Case (1)
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 302)
@@ -110,7 +117,7 @@ def test_locked_asset(self):
# Case (3)
# Enroll student
course_id = "/".join([self.loc.org, self.loc.course, self.loc.name])
- self.assertTrue(CourseEnrollment.enroll(user, course_id))
+ CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
resp = self.client.get(self.url)
diff --git a/common/test/data/toy/static/another_static.txt b/common/test/data/toy/static/another_static.txt
new file mode 100644
index 000000000000..83e560736d72
--- /dev/null
+++ b/common/test/data/toy/static/another_static.txt
@@ -0,0 +1,17 @@
+
+ _
+ | |
+ _____ ____ _ _ __ ___ _ __ | | ___
+ / _ \ \/ / _` | '_ ` _ \| '_ \| |/ _ \
+| __/> < (_| | | | | | | |_) | | __/
+ \___/_/\_\__,_|_| |_| |_| .__/|_|\___|
+ | |
+ |_|
+ _ _ _
+ | | | | (_)
+ ___| |_ __ _| |_ _ ___
+ / __| __/ _` | __| |/ __|
+ \__ \ || (_| | |_| | (__
+ |___/\__\__,_|\__|_|\___|
+
+
From 40d11c73cf87093b0c8d54db18487494e157fd35 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 16 Sep 2013 13:51:17 -0400
Subject: [PATCH 20/92] location mapper maturation richer exceptions, add_map
return course_id, fix a bug, filter out unwanted kwargs in init
---
.../xmodule/xmodule/modulestore/exceptions.py | 9 ++++++++-
.../xmodule/modulestore/loc_mapper_store.py | 19 ++++++++++++++-----
.../split_mongo/caching_descriptor_system.py | 4 +---
.../modulestore/tests/test_location_mapper.py | 8 ++++++++
4 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py
index 508599b677aa..411078d821e9 100644
--- a/common/lib/xmodule/xmodule/modulestore/exceptions.py
+++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py
@@ -28,7 +28,14 @@ class NoPathToItem(Exception):
class DuplicateItemError(Exception):
- pass
+ """
+ Attempted to create an item which already exists.
+ """
+ def __init__(self, element_id, store=None, collection=None):
+ super(DuplicateItemError, self).__init__()
+ self.element_id = element_id
+ self.store = store
+ self.collection = collection
class VersionConflictError(Exception):
diff --git a/common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py b/common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py
index 5967ee3d9d6a..4ab3a8106d6c 100644
--- a/common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py
+++ b/common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py
@@ -28,10 +28,16 @@ class LocMapperStore(object):
# C0103: varnames and attrs must be >= 3 chars, but db defined by long time usage
# pylint: disable = C0103
- def __init__(self, host, db, collection, port=27017, user=None, password=None, **kwargs):
+ def __init__(self, host, db, collection, port=27017, user=None, password=None,
+ **kwargs):
'''
Constructor
'''
+ # get rid of unwanted args
+ kwargs.pop('default_class', None)
+ kwargs.pop('fs_root', None)
+ kwargs.pop('xblock_mixins', None)
+ kwargs.pop('render_template', None)
self.db = pymongo.database.Database(
pymongo.MongoClient(
host=host,
@@ -100,6 +106,7 @@ def create_map_entry(self, course_location, course_id=None, draft_branch='draft'
'prod_branch': prod_branch,
'block_map': block_map or {},
})
+ return course_id
def translate_location(self, old_style_course_id, location, published=True, add_entry_if_missing=True):
"""
@@ -150,7 +157,7 @@ def translate_location(self, old_style_course_id, location, published=True, add_
if add_entry_if_missing:
usage_id = self._add_to_block_map(location, location_id, entry['block_map'])
else:
- raise ItemNotFoundError()
+ raise ItemNotFoundError(location)
elif isinstance(usage_id, dict):
# name is not unique, look through for the right category
if location.category in usage_id:
@@ -244,7 +251,7 @@ def add_block_location_translator(self, location, old_course_id=None, usage_id=N
if usage_id is None:
usage_id = map_entry['block_map'][location.name][location.category]
elif usage_id != map_entry['block_map'][location.name][location.category]:
- raise DuplicateItemError()
+ raise DuplicateItemError(usage_id, self, 'location_map')
computed_usage_id = usage_id
@@ -257,7 +264,7 @@ def add_block_location_translator(self, location, old_course_id=None, usage_id=N
alt_usage_id = self._verify_uniqueness(computed_usage_id, map_entry['block_map'])
if alt_usage_id != computed_usage_id:
if usage_id is not None:
- raise DuplicateItemError()
+ raise DuplicateItemError(usage_id, self, 'location_map')
else:
# revise already set ones and add to remaining ones
computed_usage_id = self.update_block_location_translator(
@@ -301,7 +308,7 @@ def update_block_location_translator(self, location, usage_id, old_course_id=Non
usage_id = self.update_block_location_translator(location, alt_usage_id, old_course_id, True)
return usage_id
else:
- raise DuplicateItemError()
+ raise DuplicateItemError(usage_id, self, 'location_map')
if location.category in map_entry['block_map'].setdefault(location.name, {}):
map_entry['block_map'][location.name][location.category] = usage_id
@@ -335,6 +342,8 @@ def _add_to_block_map(self, location, location_id, block_map):
# the block ids will likely be out of sync and collide from an id perspective. HOWEVER,
# if there are few == org/course roots or their content is unrelated, this will work well.
usage_id = self._verify_uniqueness(location.category + location.name[:3], block_map)
+ else:
+ usage_id = location.name
block_map.setdefault(location.name, {})[location.category] = usage_id
self.location_map.update(location_id, {'$set': {'block_map': block_map}})
return usage_id
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
index 020ebdbbfe57..f2979723675a 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py
@@ -30,13 +30,11 @@ def __init__(self, modulestore, course_entry, default_class, module_data, lazy,
module_data: a dict mapping Location -> json that was cached from the
underlying modulestore
"""
- # TODO find all references to resources_fs and make handle None
super(CachingDescriptorSystem, self).__init__(load_item=self._load_item, **kwargs)
self.modulestore = modulestore
self.course_entry = course_entry
self.lazy = lazy
self.module_data = module_data
- # TODO see if self.course_id is needed: is already in course_entry but could be > 1 value
# Compute inheritance
modulestore.inherit_settings(
course_entry.get('blocks', {}),
@@ -60,7 +58,7 @@ def _load_item(self, usage_id, course_entry_override=None):
self.modulestore.cache_items(self, [usage_id], lazy=self.lazy)
json_data = self.module_data.get(usage_id)
if json_data is None:
- raise ItemNotFoundError
+ raise ItemNotFoundError(usage_id)
class_ = XModuleDescriptor.load_class(
json_data.get('category'),
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_location_mapper.py b/common/lib/xmodule/xmodule/modulestore/tests/test_location_mapper.py
index 599bd6da5ede..abe41eaaed95 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_location_mapper.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_location_mapper.py
@@ -192,6 +192,14 @@ def test_translate_location_dwim(self):
add_entry_if_missing=True
)
self.assertEqual(prob_locator.course_id, new_style_course_id)
+ # create an entry w/o a guid name
+ other_location = Location('i4x', org, course, 'chapter', 'intro')
+ locator = loc_mapper().translate_location(
+ old_style_course_id,
+ other_location,
+ add_entry_if_missing=True
+ )
+ self.assertEqual(locator.usage_id, 'intro')
# add a distractor course
loc_mapper().create_map_entry(
From bd56ee21d299858582ac175c82b9784d1f34a636 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 16 Sep 2013 13:53:54 -0400
Subject: [PATCH 21/92] split get_item uses loc_mapper to decode old style
Locations
---
.../lib/xmodule/xmodule/modulestore/django.py | 6 ++++++
.../xmodule/modulestore/split_mongo/split.py | 21 +++++++++++++++----
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py
index 93416cae17ee..d9176f6abf82 100644
--- a/common/lib/xmodule/xmodule/modulestore/django.py
+++ b/common/lib/xmodule/xmodule/modulestore/django.py
@@ -75,6 +75,9 @@ def modulestore(name='default'):
if name not in _MODULESTORES:
_MODULESTORES[name] = create_modulestore_instance(settings.MODULESTORE[name]['ENGINE'],
settings.MODULESTORE[name]['OPTIONS'])
+ # inject loc_mapper into newly created modulestore if it needs it
+ if name == 'split' and _loc_singleton is not None:
+ _MODULESTORES['split'].loc_mapper = _loc_singleton
return _MODULESTORES[name]
@@ -91,6 +94,9 @@ def loc_mapper():
if _loc_singleton is None:
# instantiate
_loc_singleton = LocMapperStore(settings.modulestore_options)
+ # inject into split mongo modulestore
+ if 'split' in _MODULESTORES:
+ _MODULESTORES['split'].loc_mapper = _loc_singleton
return _loc_singleton
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index 17976abe7879..94c325e2fe93 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -5,20 +5,21 @@
import re
from importlib import import_module
from path import path
+import collections
+import copy
+from pytz import UTC
from xmodule.errortracker import null_error_tracker
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator, DescriptionLocator, CourseLocator, VersionTree, LocalId
-from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError
-from xmodule.modulestore import inheritance, ModuleStoreBase
+from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
+from xmodule.modulestore import inheritance, ModuleStoreBase, Location
from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem
from xblock.fields import Scope
from xblock.runtime import Mixologist
-from pytz import UTC
-import collections
log = logging.getLogger(__name__)
#==============================================================================
@@ -49,14 +50,17 @@ class SplitMongoModuleStore(ModuleStoreBase):
A Mongodb backed ModuleStore supporting versions, inheritance,
and sharing.
"""
+ # pylint: disable=C0103
def __init__(self, host, db, collection, fs_root, render_template,
port=27017, default_class=None,
error_tracker=null_error_tracker,
user=None, password=None,
mongo_options=None,
+ loc_mapper=None,
**kwargs):
super(SplitMongoModuleStore, self).__init__(**kwargs)
+ self.loc_mapper = loc_mapper
if mongo_options is None:
mongo_options = {}
@@ -320,6 +324,15 @@ def get_item(self, location, depth=0):
descendants.
raises InsufficientSpecificationError or ItemNotFoundError
"""
+ # intended for temporary support of some pointers being old-style
+ if isinstance(location, Location):
+ if self.loc_mapper is None:
+ raise InsufficientSpecificationError('No location mapper configured')
+ else:
+ location = self.loc_mapper.translate_location(
+ None, location, location.revision is None,
+ add_entry_if_missing=False
+ )
assert isinstance(location, BlockUsageLocator)
if not location.is_initialized():
raise InsufficientSpecificationError("Not yet initialized: %s" % location)
From 379a147a57157dc636bf1aeaca491596b227cef8 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 16 Sep 2013 13:58:38 -0400
Subject: [PATCH 22/92] Begin transactional with_version impl Change
create_item to optionally add the item to the head version and not create a
new version.
---
.../xmodule/modulestore/split_mongo/split.py | 119 ++++++++++++------
.../split_mongo/split_mongo_kvs.py | 3 +-
.../tests/test_split_modulestore.py | 91 +++++++++++++-
3 files changed, 170 insertions(+), 43 deletions(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index 94c325e2fe93..49a02d817f3b 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -123,11 +123,6 @@ def cache_items(self, system, base_usage_ids, depth=0, lazy=True):
new_module_data
)
- # remove any which were already in module_data (not sure if there's a better way)
- for newkey in new_module_data.iterkeys():
- if newkey in system.module_data:
- del new_module_data[newkey]
-
if lazy:
for block in new_module_data.itervalues():
block['definition'] = DefinitionLazyLoader(self, block['definition'])
@@ -636,45 +631,58 @@ def _generate_course_id(self, id_root):
else:
return id_root
- # TODO Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the
+ # DHM: Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the
# caller to use XModuleDescriptor.load_from_json thus reducing similar code and making the object creation and
# validation behavior a responsibility of the model layer rather than the persistence layer.
- def create_item(self, course_or_parent_locator, category, user_id, definition_locator=None, fields=None,
- force=False):
+ def create_item(self, course_or_parent_locator, category, user_id,
+ usage_id=None, definition_locator=None, fields=None,
+ force=False, continue_version=False):
"""
Add a descriptor to persistence as the last child of the optional parent_location or just as an element
of the course (if no parent provided). Return the resulting post saved version with populated locators.
- If the locator is a BlockUsageLocator, then it's assumed to be the parent. If it's a CourseLocator, then it's
+ :param course_or_parent_locator: If BlockUsageLocator, then it's assumed to be the parent.
+ If it's a CourseLocator, then it's
merely the containing course.
raises InsufficientSpecificationError if there is no course locator.
raises VersionConflictError if course_id and version_guid given and the current version head != version_guid
and force is not True.
- force: fork the structure and don't update the course draftVersion if the above
+ :param force: fork the structure and don't update the course draftVersion if the above
+ :param continue_revision: for multistep transactions, continue revising the given version rather than creating
+ a new version. Setting force to True conflicts with setting this to True and will cause a VersionConflictError
- The incoming definition_locator should either be None to indicate this is a brand new definition or
+ :param definition_locator: should either be None to indicate this is a brand new definition or
a pointer to the existing definition to which this block should point or from which this was derived.
If fields does not contain any Scope.content, then definition_locator must have a value meaning that this
block points
to the existing definition. If fields contains Scope.content and definition_locator is not None, then
the Scope.content fields are assumed to be a new payload for definition_locator.
- Creates a new version of the course structure, creates and inserts the new block, makes the block point
+ :param usage_id: if provided, must not already exist in the structure. Provides the block id for the
+ new item in this structure. Otherwise, one is computed using the category appended w/ a few digits.
+
+ :param continue_version: continue changing the current structure at the head of the course. Very dangerous
+ unless used in the same request as started the change! See below about version conflicts.
+
+ This method creates a new version of the course structure unless continue_version is True.
+ It creates and inserts the new block, makes the block point
to the definition which may be new or a new version of an existing or an existing.
+
Rules for course locator:
* If the course locator specifies a course_id and either it doesn't
- specify version_guid or the one it specifies == the current draft, it progresses the course to point
- to the new draft and sets the active version to point to the new draft
- * If the locator has a course_id but its version_guid != current draft, it raises VersionConflictError.
+ specify version_guid or the one it specifies == the current head of the branch,
+ it progresses the course to point
+ to the new head and sets the active version to point to the new head
+ * If the locator has a course_id but its version_guid != current head, it raises VersionConflictError.
NOTE: using a version_guid will end up creating a new version of the course. Your new item won't be in
the course id'd by version_guid but instead in one w/ a new version_guid. Ensure in this case that you get
the new version_guid from the locator in the returned object!
"""
# find course_index entry if applicable and structures entry
- index_entry = self._get_index_if_valid(course_or_parent_locator, force)
+ index_entry = self._get_index_if_valid(course_or_parent_locator, force, continue_version)
structure = self._lookup_course(course_or_parent_locator)
partitioned_fields = self._partition_fields_by_scope(category, fields)
@@ -686,17 +694,21 @@ def create_item(self, course_or_parent_locator, category, user_id, definition_lo
definition_locator, _ = self.update_definition_from_data(definition_locator, new_def_data, user_id)
# copy the structure and modify the new one
- new_structure = self._version_structure(structure, user_id)
+ if continue_version:
+ new_structure = structure
+ else:
+ new_structure = self._version_structure(structure, user_id)
+
# generate an id
- new_usage_id = self._generate_usage_id(new_structure['blocks'], category)
+ if usage_id is not None:
+ if usage_id in new_structure['blocks']:
+ raise DuplicateItemError(usage_id, self, 'structures')
+ else:
+ new_usage_id = usage_id
+ else:
+ new_usage_id = self._generate_usage_id(new_structure['blocks'], category)
+
update_version_keys = ['blocks.{}.edit_info.update_version'.format(new_usage_id)]
- if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.usage_id is not None:
- parent = new_structure['blocks'][course_or_parent_locator.usage_id]
- parent['fields'].setdefault('children', []).append(new_usage_id)
- parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
- parent['edit_info']['edited_by'] = user_id
- parent['edit_info']['previous_version'] = parent['edit_info']['update_version']
- update_version_keys.append('blocks.{}.edit_info.update_version'.format(course_or_parent_locator.usage_id))
block_fields = partitioned_fields.get(Scope.settings, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
@@ -710,19 +722,41 @@ def create_item(self, course_or_parent_locator, category, user_id, definition_lo
'previous_version': None
}
}
- new_id = self.structures.insert(new_structure)
+ # if given parent, add new block as child and update parent's version
+ parent = None
+ if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.usage_id is not None:
+ parent = new_structure['blocks'][course_or_parent_locator.usage_id]
+ parent['fields'].setdefault('children', []).append(new_usage_id)
+ if not continue_version or parent['edit_info']['update_version'] != structure['_id']:
+ parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
+ parent['edit_info']['edited_by'] = user_id
+ parent['edit_info']['previous_version'] = parent['edit_info']['update_version']
+ update_version_keys.append(
+ 'blocks.{}.edit_info.update_version'.format(course_or_parent_locator.usage_id)
+ )
+ if continue_version:
+ new_id = structure['_id']
+ # db update
+ self.structures.update({'_id': new_id}, new_structure)
+ # clear cache so things get refetched and inheritance recomputed
+ self._clear_cache()
+ else:
+ new_id = self.structures.insert(new_structure)
+
update_version_payload = {key: new_id for key in update_version_keys}
- self.structures.update({'_id': new_id},
+ self.structures.update(
+ {'_id': new_id},
{'$set': update_version_payload})
# update the index entry if appropriate
if index_entry is not None:
- self._update_head(index_entry, course_or_parent_locator.branch, new_id)
+ if not continue_version:
+ self._update_head(index_entry, course_or_parent_locator.branch, new_id)
course_parent = course_or_parent_locator.as_course_locator()
else:
course_parent = None
- # fetch and return the new item--fetching is unnecessary but a good qc step
+ # reconstruct the new_item from the cache
return self.get_item(BlockUsageLocator(course_id=course_parent,
usage_id=new_usage_id,
version_guid=new_id))
@@ -1245,10 +1279,9 @@ def _xblock_lists_equal(self, lista, listb):
"""
if len(lista) != len(listb):
return False
- for idx in enumerate(lista):
- if lista[idx] != listb[idx]:
- itema = self._usage_id(lista[idx])
- if itema != self._usage_id(listb[idx]):
+ for ele_a, ele_b in zip(lista, listb):
+ if ele_a != ele_b:
+ if self._usage_id(ele_a) != self._usage_id(ele_b):
return False
return True
@@ -1262,22 +1295,31 @@ def _usage_id(self, xblock_or_id):
else:
return xblock_or_id
- def _get_index_if_valid(self, locator, force=False):
+ def _get_index_if_valid(self, locator, force=False, continue_version=False):
"""
If the locator identifies a course and points to its draft (or plausibly its draft),
then return the index entry.
raises VersionConflictError if not the right version
- :param locator:
+ :param locator: a courselocator
+ :param force: if false, raises VersionConflictError if the current head of the course != the one identified
+ by locator. Cannot be True if continue_version is True
+ :param continue_version: if True, assumes this operation requires a head version and will not create a new
+ version but instead continue an existing transaction on this version. This flag cannot be True if force is True.
"""
if locator.course_id is None or locator.branch is None:
- return None
+ if continue_version:
+ raise InsufficientSpecificationError(
+ "To continue a version, the locator must point to one ({}).".format(locator)
+ )
+ else:
+ return None
else:
index_entry = self.course_index.find_one({'_id': locator.course_id})
if (locator.version_guid is not None
and index_entry['versions'][locator.branch] != locator.version_guid
- and not force):
+ and not force) or (force and continue_version):
raise VersionConflictError(
locator,
CourseLocator(
@@ -1293,8 +1335,7 @@ def _version_structure(self, structure, user_id):
:param structure:
:param user_id:
"""
- new_structure = structure.copy()
- new_structure['blocks'] = new_structure['blocks'].copy()
+ new_structure = copy.deepcopy(structure)
del new_structure['_id']
new_structure['previous_version'] = structure['_id']
new_structure['edited_by'] = user_id
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
index e2d8fd30a9be..bfec024a1647 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_mongo_kvs.py
@@ -25,7 +25,8 @@ def __init__(self, definition, fields, inherited_settings):
Note, local fields may override and disagree w/ this b/c this says what the value
should be if the field is undefined.
"""
- super(SplitMongoKVS, self).__init__(copy.copy(fields), inherited_settings)
+ # deepcopy so that manipulations of fields does not pollute the source
+ super(SplitMongoKVS, self).__init__(copy.deepcopy(fields), inherited_settings)
self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
# if the db id, then the definition is presumed to be loaded into _fields
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
index 832fd2091e5f..db4f6d2adccd 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
@@ -11,7 +11,8 @@
from xblock.fields import Scope
from xmodule.course_module import CourseDescriptor
-from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError, VersionConflictError
+from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError, VersionConflictError, \
+ DuplicateItemError
from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, DescriptionLocator
from xmodule.modulestore.inheritance import InheritanceMixin
from pytz import UTC
@@ -492,7 +493,7 @@ class TestItemCrud(SplitModuleTest):
"""
Test create update and delete of items
"""
- # TODO do I need to test this case which I believe won't work:
+ # DHM do I need to test this case which I believe won't work:
# 1) fetch a course and some of its blocks
# 2) do a series of CRUD operations on those previously fetched elements
# The problem here will be that the version_guid of the items will be the version at time of fetch.
@@ -604,7 +605,91 @@ def test_unique_naming(self):
self.assertGreaterEqual(new_history['edited_on'], premod_time)
another_history = modulestore().get_definition_history_info(another_module.definition_locator)
self.assertEqual(another_history['previous_version'], 'problem12345_3_1')
- # TODO check that default fields are set
+
+ def test_create_continue_version(self):
+ """
+ Test create_item using the continue_version flag
+ """
+ # start transaction w/ simple creation
+ user = random.getrandbits(32)
+ new_course = modulestore().create_course('test_org', 'test_transaction', user)
+ new_course_locator = new_course.location.as_course_locator()
+ index_history_info = modulestore().get_course_history_info(new_course.location)
+ course_block_prev_version = new_course.previous_version
+ course_block_update_version = new_course.update_version
+ self.assertIsNotNone(new_course_locator.version_guid, "Want to test a definite version")
+ versionless_course_locator = CourseLocator(
+ course_id=new_course_locator.course_id, branch=new_course_locator.branch
+ )
+
+ # positive simple case: no force, add chapter
+ new_ele = modulestore().create_item(
+ new_course.location, 'chapter', user,
+ fields={'display_name': 'chapter 1'},
+ continue_version=True
+ )
+ # version info shouldn't change
+ self.assertEqual(new_ele.update_version, course_block_update_version)
+ self.assertEqual(new_ele.update_version, new_ele.location.version_guid)
+ refetch_course = modulestore().get_course(versionless_course_locator)
+ self.assertEqual(refetch_course.location.version_guid, new_course.location.version_guid)
+ self.assertEqual(refetch_course.previous_version, course_block_prev_version)
+ self.assertEqual(refetch_course.update_version, course_block_update_version)
+ refetch_index_history_info = modulestore().get_course_history_info(refetch_course.location)
+ self.assertEqual(refetch_index_history_info, index_history_info)
+ self.assertIn(new_ele.location.usage_id, refetch_course.children)
+
+ # try to create existing item
+ with self.assertRaises(DuplicateItemError):
+ _fail = modulestore().create_item(
+ new_course.location, 'chapter', user,
+ usage_id=new_ele.location.usage_id,
+ fields={'display_name': 'chapter 2'},
+ continue_version=True
+ )
+
+ # ensure force w/ continue gives exception
+ with self.assertRaises(VersionConflictError):
+ _fail = modulestore().create_item(
+ new_course.location, 'chapter', user,
+ fields={'display_name': 'chapter 2'},
+ force=True, continue_version=True
+ )
+
+ # start a new transaction
+ new_ele = modulestore().create_item(
+ new_course.location, 'chapter', user,
+ fields={'display_name': 'chapter 2'},
+ continue_version=False
+ )
+ transaction_guid = new_ele.location.version_guid
+ # ensure trying to continue the old one gives exception
+ with self.assertRaises(VersionConflictError):
+ _fail = modulestore().create_item(
+ new_course.location, 'chapter', user,
+ fields={'display_name': 'chapter 3'},
+ continue_version=True
+ )
+
+ # add new child to old parent in continued (leave off version_guid)
+ course_module_locator = BlockUsageLocator(
+ course_id=new_course.location.course_id,
+ usage_id=new_course.location.usage_id,
+ branch=new_course.location.branch
+ )
+ new_ele = modulestore().create_item(
+ course_module_locator, 'chapter', user,
+ fields={'display_name': 'chapter 4'},
+ continue_version=True
+ )
+ self.assertNotEqual(new_ele.update_version, course_block_update_version)
+ self.assertEqual(new_ele.location.version_guid, transaction_guid)
+
+ # check children, previous_version
+ refetch_course = modulestore().get_course(versionless_course_locator)
+ self.assertIn(new_ele.location.usage_id, refetch_course.children)
+ self.assertEqual(refetch_course.previous_version, course_block_update_version)
+ self.assertEqual(refetch_course.update_version, transaction_guid)
def test_update_metadata(self):
"""
From ad2da44cb1cffac8ec8b45d38bc0fde3773023c1 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 19 Sep 2013 13:06:55 -0400
Subject: [PATCH 23/92] Make Textbooks properly lazy
---
common/lib/xmodule/xmodule/course_module.py | 15 +++++---
common/lib/xmodule/xmodule/util/decorators.py | 37 -------------------
requirements/edx/base.txt | 1 +
3 files changed, 11 insertions(+), 42 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/util/decorators.py
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 24e6995ae105..6d6d204eb810 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -6,10 +6,10 @@
import requests
from datetime import datetime
import dateutil.parser
+from lazy import lazy
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
-from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
import json
@@ -62,17 +62,22 @@ class Textbook(object):
def __init__(self, title, book_url):
self.title = title
self.book_url = book_url
- self.start_page = int(self.table_of_contents[0].attrib['page'])
+ @lazy
+ def start_page(self):
+ return int(self.table_of_contents[0].attrib['page'])
+
+ @lazy
+ def end_page(self):
# The last page should be the last element in the table of contents,
# but it may be nested. So recurse all the way down the last element
last_el = self.table_of_contents[-1]
while last_el.getchildren():
last_el = last_el[-1]
- self.end_page = int(last_el.attrib['page'])
+ return int(last_el.attrib['page'])
- @lazyproperty
+ @lazy
def table_of_contents(self):
"""
Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
@@ -738,7 +743,7 @@ def _sorting_dates(self):
return announcement, start, now
- @lazyproperty
+ @lazy
def grading_context(self):
"""
This returns a dictionary with keys necessary for quickly grading
diff --git a/common/lib/xmodule/xmodule/util/decorators.py b/common/lib/xmodule/xmodule/util/decorators.py
deleted file mode 100644
index e827b74ef17a..000000000000
--- a/common/lib/xmodule/xmodule/util/decorators.py
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-def lazyproperty(fn):
- """
- Use this decorator for lazy generation of properties that
- are expensive to compute. From http://stackoverflow.com/a/3013910/86828
-
-
- Example:
- class Test(object):
-
- @lazyproperty
- def a(self):
- print 'generating "a"'
- return range(5)
-
- Interactive Session:
- >>> t = Test()
- >>> t.__dict__
- {}
- >>> t.a
- generating "a"
- [0, 1, 2, 3, 4]
- >>> t.__dict__
- {'_lazy_a': [0, 1, 2, 3, 4]}
- >>> t.a
- [0, 1, 2, 3, 4]
- """
-
- attr_name = '_lazy_' + fn.__name__
-
- @property
- def _lazyprop(self):
- if not hasattr(self, attr_name):
- setattr(self, attr_name, fn(self))
- return getattr(self, attr_name)
- return _lazyprop
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 62515f74450c..addef7b629cc 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -34,6 +34,7 @@ feedparser==5.1.3
fs==0.4.0
GitPython==0.3.2.RC1
glob2==0.3
+lazy==1.1
lxml==3.0.1
mako==0.7.3
Markdown==2.2.1
From cfa2b27f26ac482c6b4d30f4073f04160f781e6a Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 19 Sep 2013 13:07:10 -0400
Subject: [PATCH 24/92] Turn Locations into dicts less often
---
common/lib/xmodule/xmodule/modulestore/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index e53b4adb5e8b..808932988ab7 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -204,7 +204,7 @@ def url(self):
"""
Return a string containing the URL for this location
"""
- url = "{tag}://{org}/{course}/{category}/{name}".format(**self.dict())
+ url = "{0.tag}://{0.org}/{0.course}/{0.category}/{0.name}".format(self)
if self.revision:
url += "@" + self.revision
return url
From e5c90d33fc551cc6871a13c5c5d5b34b9530ae58 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Mon, 23 Sep 2013 14:21:24 -0400
Subject: [PATCH 25/92] Fix middleware order in CMS.
And include Don's fix for partial course_id lookup.
---
cms/envs/common.py | 2 +-
common/djangoapps/contentserver/middleware.py | 8 +++---
common/djangoapps/contentserver/tests/test.py | 9 +++----
common/djangoapps/student/models.py | 25 +++++++++++++++++++
4 files changed, 33 insertions(+), 11 deletions(-)
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 38913f537ab4..c79cfe7aa78d 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -140,7 +140,6 @@
)
MIDDLEWARE_CLASSES = (
- 'contentserver.middleware.StaticContentServer',
'request_cache.middleware.RequestCache',
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -150,6 +149,7 @@
# Instead of AuthenticationMiddleware, we use a cache-backed version
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
+ 'contentserver.middleware.StaticContentServer',
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py
index 8d81929693cb..30ab977b9683 100644
--- a/common/djangoapps/contentserver/middleware.py
+++ b/common/djangoapps/contentserver/middleware.py
@@ -46,10 +46,10 @@ def process_request(self, request):
# Check that user has access to content
if getattr(content, "locked", False):
if not hasattr(request, "user") or not request.user.is_authenticated():
- return redirect('login')
- course_id = "/".join([loc.org, loc.course, loc.name])
- if not CourseEnrollment.is_enrolled(request.user, course_id):
- return redirect('dashboard')
+ return HttpResponse('Unauthorized', status=403)
+ course_partial_id = "/".join([loc.org, loc.course])
+ if not CourseEnrollment.is_enrolled_by_partial(request.user, course_partial_id):
+ return HttpResponse('Unauthorized', status=403)
# see if the last-modified at hasn't changed, if not return a 302 (Not Modified)
diff --git a/common/djangoapps/contentserver/tests/test.py b/common/djangoapps/contentserver/tests/test.py
index 340092b4d346..7313ba3f9b96 100644
--- a/common/djangoapps/contentserver/tests/test.py
+++ b/common/djangoapps/contentserver/tests/test.py
@@ -94,8 +94,7 @@ def test_locked_asset(self):
# Case (1)
resp = self.client.get(self.url)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(resp.has_header("LOCATION"))
+ self.assertEqual(resp.status_code, 403)
# Case (2)
# Create user and login
@@ -110,13 +109,11 @@ def test_locked_asset(self):
resp = self.client.get(self.url)
log.debug("Received response %s", resp)
- self.assertEqual(resp.status_code, 302)
- self.assertTrue(resp.has_header("LOCATION"))
- self.assertIn("dashboard", resp["LOCATION"])
+ self.assertEqual(resp.status_code, 403)
# Case (3)
# Enroll student
- course_id = "/".join([self.loc.org, self.loc.course, self.loc.name])
+ course_id = "/".join([self.loc.org, self.loc.course, '2012_Fall'])
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 71cff4183b61..8cda698d8b89 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -843,6 +843,31 @@ def is_enrolled(cls, user, course_id):
except cls.DoesNotExist:
return False
+ @classmethod
+ def is_enrolled_by_partial(cls, user, course_id_partial):
+ """
+ Returns `True` if the user is enrolled in a course that starts with
+ `course_id_partial`. Otherwise, returns False.
+
+ Can be used to determine whether a student is enrolled in a course
+ whose run name is unknown.
+
+ `user` is a Django User object. If it hasn't been saved yet (no `.id`
+ attribute), this method will automatically save it before
+ adding an enrollment for it.
+
+ `course_id_partial` is a starting substring for a fully qualified
+ course_id (e.g. "edX/Test101/").
+ """
+ try:
+ return CourseEnrollment.objects.filter(
+ user=user,
+ course_id__startswith=course_id_partial,
+ is_active=1
+ ).exists()
+ except cls.DoesNotExist:
+ return False
+
@classmethod
def enrollment_mode_for_user(cls, user, course_id):
"""
From 5347a49975e181c05235f3d4eab74b8519d76523 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Mon, 23 Sep 2013 16:50:02 -0400
Subject: [PATCH 26/92] Disable test that fails intermittently in master
---
.../lib/xmodule/xmodule/js/spec/video/video_caption_spec.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index e803a745e0e3..a94a35e12437 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -645,7 +645,7 @@
});
});
- describe('when enter key is pressed on a caption', function() {
+ xdescribe('when enter key is pressed on a caption', function() {
beforeEach(function() {
var e;
spyOn(videoCaption, 'seekPlayer').andCallThrough();
@@ -663,7 +663,7 @@
expect($('.subtitles li[data-index=0]')).toHaveClass('focused');
});
- it('calls seekPlayer', function() {
+ xit('calls seekPlayer', function() {
expect(videoCaption.seekPlayer).toHaveBeenCalled();
});
});
From f8cd482621ab0b5ff1d0293578351e5952cdf6e8 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 23 Sep 2013 16:56:30 -0400
Subject: [PATCH 27/92] Replace date lib w/ simpler tzAbbr
---
.../js/views/settings/main_settings_view.js | 3 +-
cms/templates/settings.html | 2 +-
common/static/js/vendor/tzAbbr.js | 59 +++++++++++++++++++
3 files changed, 61 insertions(+), 3 deletions(-)
create mode 100644 common/static/js/vendor/tzAbbr.js
diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js
index 84244c6e7b8c..3d5861ca259e 100644
--- a/cms/static/js/views/settings/main_settings_view.js
+++ b/cms/static/js/views/settings/main_settings_view.js
@@ -33,8 +33,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
$(this).show();
});
- var dateIntrospect = new Date();
- this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
+ this.$el.find('#timezone').html("(" + tzAbbr() + ")");
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.listenTo(this.model, 'change', this.showNotificationBar);
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index a8dc48dd18d8..eff917695629 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -14,7 +14,7 @@
-
+
diff --git a/common/static/js/vendor/tzAbbr.js b/common/static/js/vendor/tzAbbr.js
new file mode 100644
index 000000000000..18eb5947ee54
--- /dev/null
+++ b/common/static/js/vendor/tzAbbr.js
@@ -0,0 +1,59 @@
+/* Friendly timezone abbreviations in client-side JavaScript
+
+`tzAbbr()` or `tzAbbr(new Date(79,5,24))`
+=> "EDT", "CST", "GMT", etc.!
+
+There's no 100% reliable way to get friendly timezone names in all
+browsers using JS alone, but this tiny function scours a
+stringified date as best it can and returns `null` in the few cases
+where no friendly timezone name is found (so far, just Opera).
+
+Device tested & works in:
+* IE 6, 7, 8, and 9 (latest versions of all)
+* Firefox 3 [through] 16 (16 = latest version to date)
+* Chrome 22 (latest version to date)
+* Safari 6 (latest version to date)
+* Mobile Safari on iOS 5 & 6
+* Android 4.0.3 stock browser
+* Android 2.3.7 stock browser
+* IE Mobile 9 (WP 7.5)
+
+Known to fail in:
+* Opera 12 (desktop, latest version to date)
+
+For Opera, I've included (but commented out) a workaround spotted
+on StackOverflow that returns a GMT offset when no abbreviation is
+found. I haven't found a decent workaround.
+
+If you find any other cases where this method returns null or dodgy
+results, please say so in the comments; even if we can't find a
+workaround it'll at least help others determine if this approach is
+suitable for their project!
+*/
+var tzAbbr = function (dateInput) {
+ var dateObject = dateInput || new Date(),
+ dateString = dateObject + "",
+ tzAbbr = (
+ // Works for the majority of modern browsers
+ dateString.match(/\(([^\)]+)\)$/) ||
+ // IE outputs date strings in a different format:
+ dateString.match(/([A-Z]+) [\d]{4}$/)
+ );
+
+ if (tzAbbr) {
+ // Old Firefox uses the long timezone name (e.g., "Central
+ // Daylight Time" instead of "CDT")
+ tzAbbr = tzAbbr[1].match(/[A-Z]/g).join("");
+ }
+
+ // Uncomment these lines to return a GMT offset for browsers
+ // that don't include the user's zone abbreviation (e.g.,
+ // "GMT-0500".) I prefer to have `null` in this case, but
+ // you may not!
+ // First seen on: http://stackoverflow.com/a/12496442
+ // if (!tzAbbr && /(GMT\W*\d{4})/.test(dateString)) {
+ // return RegExp.$1;
+ // }
+
+ return tzAbbr;
+};
\ No newline at end of file
From 9836fd51c7646e001c97747897ff82f015e96d9c Mon Sep 17 00:00:00 2001
From: jmclaus
Date: Sat, 21 Sep 2013 00:20:48 +0200
Subject: [PATCH 28/92] Speed control now regains focus after a speed entry has
been selected with the keyboard
---
.../lib/xmodule/xmodule/js/src/video/08_video_speed_control.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
index 9db56ffc0d9b..67f62edf95b8 100644
--- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
@@ -283,6 +283,9 @@ function () {
this.videoSpeedControl.currentSpeed
);
}
+ // When a speed entry has been selected, we want the speed control to
+ // regain focus.
+ parentEl.parent().siblings('a').focus();
}
function reRender(params) {
From ffa49b03910891ffa8595e453971b2d2ae8c8a85 Mon Sep 17 00:00:00 2001
From: jmclaus
Date: Mon, 23 Sep 2013 17:37:35 +0200
Subject: [PATCH 29/92] Added tests
---
.../js/spec/video/video_speed_control_spec.js | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index af919e135ef6..4b650d11bec6 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -138,6 +138,28 @@
expect(videoSpeedControl.currentSpeed).toEqual(0.75);
});
});
+
+ describe('make sure the speed control gets the focus afterwards', function() {
+ var focusSpy;
+
+ beforeEach(function() {
+ initialize();
+ videoSpeedControl.setSpeed(1.0);
+ focusSpy = spyOn($.fn, 'focus').andCallThrough();
+ });
+
+ it('when the speed is the same', function() {
+ $('li[data-speed="1.0"] a').click();
+ expect(focusSpy).toHaveBeenCalled();
+ expect(focusSpy.mostRecentCall.object.selector).toEqual('.parent().parent().siblings(a)');
+ });
+
+ it('when the speed is not the same', function() {
+ $('li[data-speed="0.75"] a').click();
+ expect(focusSpy).toHaveBeenCalled();
+ expect(focusSpy.mostRecentCall.object.selector).toEqual('.parent().parent().siblings(a)');
+ });
+ });
});
describe('onSpeedChange', function() {
From 2cc9679ea8f47114c1809e4a9a221eb1698e0c92 Mon Sep 17 00:00:00 2001
From: jmclaus
Date: Tue, 24 Sep 2013 09:12:33 +0200
Subject: [PATCH 30/92] Used Jasmine jQuery for tests instead
---
.../xmodule/js/spec/video/video_speed_control_spec.js | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index 4b650d11bec6..a4369005cc9d 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -140,24 +140,22 @@
});
describe('make sure the speed control gets the focus afterwards', function() {
- var focusSpy;
+ var spyEvent;
beforeEach(function() {
initialize();
videoSpeedControl.setSpeed(1.0);
- focusSpy = spyOn($.fn, 'focus').andCallThrough();
+ spyEvent = spyOnEvent('.speeds > a', 'focus');
});
it('when the speed is the same', function() {
$('li[data-speed="1.0"] a').click();
- expect(focusSpy).toHaveBeenCalled();
- expect(focusSpy.mostRecentCall.object.selector).toEqual('.parent().parent().siblings(a)');
+ expect(spyEvent).toHaveBeenTriggered();
});
it('when the speed is not the same', function() {
$('li[data-speed="0.75"] a').click();
- expect(focusSpy).toHaveBeenCalled();
- expect(focusSpy.mostRecentCall.object.selector).toEqual('.parent().parent().siblings(a)');
+ expect(spyEvent).toHaveBeenTriggered();;
});
});
});
From feb95eb86965f573e3e4a9c6814e4706b8306532 Mon Sep 17 00:00:00 2001
From: jmclaus
Date: Tue, 24 Sep 2013 10:30:45 +0200
Subject: [PATCH 31/92] Fixed tests
---
.../xmodule/js/spec/video/video_speed_control_spec.js | 10 ++++------
.../xmodule/js/src/video/08_video_speed_control.js | 2 ++
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index a4369005cc9d..13a5705238f4 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -140,22 +140,20 @@
});
describe('make sure the speed control gets the focus afterwards', function() {
- var spyEvent;
-
- beforeEach(function() {
+ beforeEach(function() {
initialize();
videoSpeedControl.setSpeed(1.0);
- spyEvent = spyOnEvent('.speeds > a', 'focus');
+ spyOnEvent(state.videoSpeedControl.mainAnchor, 'focus');
});
it('when the speed is the same', function() {
$('li[data-speed="1.0"] a').click();
- expect(spyEvent).toHaveBeenTriggered();
+ expect('focus').toHaveBeenTriggeredOn(state.videoSpeedControl.mainAnchor);
});
it('when the speed is not the same', function() {
$('li[data-speed="0.75"] a').click();
- expect(spyEvent).toHaveBeenTriggered();;
+ expect('focus').toHaveBeenTriggeredOn(state.videoSpeedControl.mainAnchor);
});
});
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
index 67f62edf95b8..2ba8a301445d 100644
--- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
@@ -62,6 +62,8 @@ function () {
state.videoSpeedControl.el = state.el.find('div.speeds');
+ state.videoSpeedControl.mainAnchor = state.videoSpeedControl.el.children('a');
+
state.videoSpeedControl.videoSpeedsEl = state.videoSpeedControl.el
.find('.video_speeds');
From 3a5d7d8638f9253500d23df926019c79252263fe Mon Sep 17 00:00:00 2001
From: jmclaus
Date: Tue, 24 Sep 2013 12:12:10 +0200
Subject: [PATCH 32/92] Addressed PR comments
---
.../xmodule/js/spec/video/video_speed_control_spec.js | 10 ++++++----
.../xmodule/js/src/video/08_video_speed_control.js | 2 --
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index 13a5705238f4..2684fb738e47 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -140,20 +140,22 @@
});
describe('make sure the speed control gets the focus afterwards', function() {
- beforeEach(function() {
+ var anchor;
+ beforeEach(function() {
initialize();
+ anchor= $('.speeds > a').first();
videoSpeedControl.setSpeed(1.0);
- spyOnEvent(state.videoSpeedControl.mainAnchor, 'focus');
+ spyOnEvent(anchor, 'focus');
});
it('when the speed is the same', function() {
$('li[data-speed="1.0"] a').click();
- expect('focus').toHaveBeenTriggeredOn(state.videoSpeedControl.mainAnchor);
+ expect('focus').toHaveBeenTriggeredOn(anchor);
});
it('when the speed is not the same', function() {
$('li[data-speed="0.75"] a').click();
- expect('focus').toHaveBeenTriggeredOn(state.videoSpeedControl.mainAnchor);
+ expect('focus').toHaveBeenTriggeredOn(anchor);
});
});
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
index 2ba8a301445d..67f62edf95b8 100644
--- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
@@ -62,8 +62,6 @@ function () {
state.videoSpeedControl.el = state.el.find('div.speeds');
- state.videoSpeedControl.mainAnchor = state.videoSpeedControl.el.children('a');
-
state.videoSpeedControl.videoSpeedsEl = state.videoSpeedControl.el
.find('.video_speeds');
From 3b61d5721aec18abb33c1fcd4aa5a837869ff103 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Tue, 24 Sep 2013 09:03:36 -0400
Subject: [PATCH 33/92] Ensure location is valid before trying to fetch Prevent
accidental wildcarding.
---
common/lib/xmodule/xmodule/contentstore/mongo.py | 2 ++
.../xmodule/xmodule/modulestore/tests/test_mongo.py | 10 ++++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py
index 40c0b4bc9f93..e146e46e3207 100644
--- a/common/lib/xmodule/xmodule/contentstore/mongo.py
+++ b/common/lib/xmodule/xmodule/contentstore/mongo.py
@@ -177,6 +177,8 @@ def set_attrs(self, location, attr_dict):
:param location: a c4x asset location
"""
+ # raises exception if location is not fully specified
+ Location.ensure_fully_specified(location)
for attr in attr_dict.iterkeys():
if attr in ['_id', 'md5', 'uploadDate', 'length']:
raise AttributeError("{} is a protected attribute.".format(attr))
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index e89aa21a167f..c2e6be279354 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -22,6 +22,7 @@
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
from IPython.testing.nose_assert_methods import assert_in, assert_not_in
from xmodule.exceptions import NotFoundError
+from xmodule.modulestore.exceptions import InsufficientSpecificationError
log = logging.getLogger(__name__)
@@ -227,11 +228,11 @@ def test_contentstore_attrs(self):
TestMongoModuleStore.content_store.set_attrs(content['_id'], {'miscel': 99})
assert_equals(TestMongoModuleStore.content_store.get_attr(content['_id'], 'miscel'), 99)
assert_raises(
- AttributeError, TestMongoModuleStore.content_store.set_attr, course_content[0],
+ AttributeError, TestMongoModuleStore.content_store.set_attr, course_content[0]['_id'],
'md5', 'ff1532598830e3feac91c2449eaa60d6'
)
assert_raises(
- AttributeError, TestMongoModuleStore.content_store.set_attrs, course_content[0],
+ AttributeError, TestMongoModuleStore.content_store.set_attrs, course_content[0]['_id'],
{'foo': 9, 'md5': 'ff1532598830e3feac91c2449eaa60d6'}
)
assert_raises(
@@ -253,6 +254,11 @@ def test_contentstore_attrs(self):
Location('bogus', 'bogus', 'bogus', 'asset', 'bogus'),
{'displayname': 'hello'}
)
+ assert_raises(
+ InsufficientSpecificationError, TestMongoModuleStore.content_store.set_attrs,
+ Location('bogus', 'bogus', 'bogus', 'asset', None),
+ {'displayname': 'hello'}
+ )
class TestMongoKeyValueStore(object):
From f30364906da8f2555d6596051b6d2c157dc16214 Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Fri, 20 Sep 2013 10:38:28 -0400
Subject: [PATCH 34/92] Speed up lti acceptance test for remote webdriver.
---
lms/djangoapps/courseware/features/lti.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py
index 0e91d5ed0266..d9cc40f69073 100644
--- a/lms/djangoapps/courseware/features/lti.py
+++ b/lms/djangoapps/courseware/features/lti.py
@@ -22,7 +22,10 @@ def lti_is_not_rendered(_step):
#inside iframe test content is not presented
with world.browser.get_iframe('ltiLaunchFrame') as iframe:
# iframe does not contain functions from terrain/ui_helpers.py
- assert iframe.is_element_not_present_by_css('.result', wait_time=5)
+ world.browser.driver.implicitly_wait(1)
+ result = iframe.is_element_not_present_by_css('.result', wait_time=1)
+ world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
+ assert result
@step('I view the LTI and it is rendered$')
From 20ea28748bd7c96236906d3b55614eb486b3c6d5 Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Thu, 19 Sep 2013 12:54:45 -0400
Subject: [PATCH 35/92] Speed up problems feature tests
---
lms/djangoapps/courseware/features/problems_setup.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py
index 0253571b479e..3b1b983fc38c 100644
--- a/lms/djangoapps/courseware/features/problems_setup.py
+++ b/lms/djangoapps/courseware/features/problems_setup.py
@@ -144,7 +144,7 @@ def test_add_to_ten(expect,ans):
]
},
'correct': ['section.choicetextgroup_correct'],
- 'incorrect': ['span.incorrect', 'section.choicetextgroup_incorrect'],
+ 'incorrect': ['section.choicetextgroup_incorrect', 'span.incorrect'],
'unanswered': ['span.unanswered']},
'checkbox_text': {
@@ -242,7 +242,7 @@ def answer_problem(problem_type, correctness):
def problem_has_answer(problem_type, answer_class):
if problem_type == "drop down":
if answer_class == 'blank':
- assert world.browser.is_element_not_present_by_css('option[selected="true"]')
+ assert world.is_css_not_present('option[selected="true"]')
else:
actual = world.browser.find_by_css('option[selected="true"]').value
expected = 'Option 2' if answer_class == 'correct' else 'Option 3'
From 002f3ef9e27a80884ea664330604397b71bc5a4d Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Thu, 19 Sep 2013 11:51:34 -0400
Subject: [PATCH 36/92] Speed up waiting for elements not present on remote
webdriver sessions.
---
cms/djangoapps/contentstore/features/subsection.py | 2 +-
common/djangoapps/terrain/browser.py | 6 ++++--
common/djangoapps/terrain/ui_helpers.py | 9 +++++++--
lms/djangoapps/courseware/features/lti.py | 9 ++++++---
4 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py
index 6d9612d9bdd9..68e65ee7ace6 100644
--- a/cms/djangoapps/contentstore/features/subsection.py
+++ b/cms/djangoapps/contentstore/features/subsection.py
@@ -109,7 +109,7 @@ def i_see_my_subsection_name_with_quote_on_the_courseware_page(step):
@step('the subsection does not exist$')
def the_subsection_does_not_exist(step):
css = 'span.subsection-name'
- assert world.browser.is_element_not_present_by_css(css)
+ assert world.is_css_not_present(css)
@step('I see the subsection release date is ([0-9/-]+)( [0-9:]+)?')
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index e8d5b1f2e763..f40108eb3aca 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -119,6 +119,7 @@ def initial_setup(server):
if not success:
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
+ world.absorb(0, 'IMPLICIT_WAIT')
world.browser.driver.set_window_size(1280, 1024)
elif world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
@@ -128,7 +129,7 @@ def initial_setup(server):
url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
**make_saucelabs_desired_capabilities()
)
- world.browser.driver.implicitly_wait(30)
+ world.absorb(30, 'IMPLICIT_WAIT')
elif world.LETTUCE_SELENIUM_CLIENT == 'grid':
world.browser = Browser(
@@ -136,11 +137,12 @@ def initial_setup(server):
url=settings.SELENIUM_GRID.get('URL'),
browser=settings.SELENIUM_GRID.get('BROWSER'),
)
- world.browser.driver.implicitly_wait(30)
+ world.absorb(30, 'IMPLICIT_WAIT')
else:
raise Exception("Unknown selenium client '{}'".format(world.LETTUCE_SELENIUM_CLIENT))
+ world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
world.absorb(world.browser.driver.session_id, 'jobid')
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
index e4e0626779ee..75bc8bb2cb37 100644
--- a/common/djangoapps/terrain/ui_helpers.py
+++ b/common/djangoapps/terrain/ui_helpers.py
@@ -36,8 +36,13 @@ def is_css_present(css_selector, wait_time=10):
@world.absorb
def is_css_not_present(css_selector, wait_time=5):
- return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
-
+ world.browser.driver.implicitly_wait(1)
+ try:
+ return world.browser.is_element_not_present_by_css(css_selector, wait_time=wait_time)
+ except:
+ raise
+ finally:
+ world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
@world.absorb
def css_has_text(css_selector, text, index=0):
diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py
index d9cc40f69073..ef385cf2c9bb 100644
--- a/lms/djangoapps/courseware/features/lti.py
+++ b/lms/djangoapps/courseware/features/lti.py
@@ -23,9 +23,12 @@ def lti_is_not_rendered(_step):
with world.browser.get_iframe('ltiLaunchFrame') as iframe:
# iframe does not contain functions from terrain/ui_helpers.py
world.browser.driver.implicitly_wait(1)
- result = iframe.is_element_not_present_by_css('.result', wait_time=1)
- world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
- assert result
+ try:
+ assert iframe.is_element_not_present_by_css('.result', wait_time=1)
+ except:
+ raise
+ finally:
+ world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
@step('I view the LTI and it is rendered$')
From 818f4f231ae239239cdd45b53377f3dc17321274 Mon Sep 17 00:00:00 2001
From: stroilova
Date: Tue, 27 Aug 2013 12:45:16 -0400
Subject: [PATCH 37/92] Update event doc from wiki
update to grid table
Cleaned up formatting to use `` instead of :code:
Added Instructor Events and cleaned up outline
Added instructor events. Cleaned up outline.
Moved out correct_map table to addendum
Removed deprecated/undocumented events
---
docs/data/source/index.rst | 2 +-
.../internal_data_formats/tracking_logs.rst | 695 ++++++++----------
2 files changed, 313 insertions(+), 384 deletions(-)
diff --git a/docs/data/source/index.rst b/docs/data/source/index.rst
index 468731f255ee..d1efb4690257 100644
--- a/docs/data/source/index.rst
+++ b/docs/data/source/index.rst
@@ -5,7 +5,7 @@
edX Data Documentation
======================
-The following documents are targetted at those who are working with various data formats consumed and produced by the edX platform -- primarily course authors and those who are conducting research on data in our system. Developer oriented discussion of architecture and strictly internal APIs should be documented elsewhere.
+The following documents are targeted at those who are working with various data formats consumed and produced by the edX platform -- primarily course authors and those who are conducting research on data in our system. Developer oriented discussion of architecture and strictly internal APIs should be documented elsewhere.
Course Data Formats
-------------------
diff --git a/docs/data/source/internal_data_formats/tracking_logs.rst b/docs/data/source/internal_data_formats/tracking_logs.rst
index 04326497d74c..7af67cbc82bd 100644
--- a/docs/data/source/internal_data_formats/tracking_logs.rst
+++ b/docs/data/source/internal_data_formats/tracking_logs.rst
@@ -1,389 +1,318 @@
-#############
-Tracking Logs
-#############
+===============
+ Tracking Logs
+===============
-* Tracking logs are made available as separate tar files on S3 in the course-data bucket.
-* They are represented as JSON files that catalog all user interactions with the site.
-* To avoid filename collisions the tracking logs are organized by server name, where each directory corresponds to a server where they were stored.
+The following is an inventory of all LMS event types.
-*************
-Common Fields
-*************
-
- .. list-table::
- :widths: 10 40 10 25
- :header-rows: 1
-
- * - field
- - details
- - type
- - values/format
- * - `username`
- - username of the user who triggered the event, empty string for anonymous events (not logged in)
- - string
- -
- * - `session`
- - key identifying the user's session, may be undefined
- - string
- - 32 digits key
- * - `time`
- - GMT time the event was triggered
- - string
- - `YYYY-MM-DDThh:mm:ss.xxxxxx`
- * - `ip`
- - user ip address
- - string
- -
- * - `agent`
- - users browser agent string
- - string
- -
- * - `page`
- - page the user was visiting when the event was generated
- - string
- - `$URL`
- * - event_source
- - event source
- - string
- - `browser`, `server`
- * - `event_type`
- - type of event triggered, values depends on `event_source`
- - string
- - *more details listed below*
- * - `event`
- - specifics of the event (dependenty of the event_type)
- - string/json
- - *the event string may encode a JSON record*
-
-
-*************
-Event Sources
-*************
+This inventory is comprised of a table of Common Fields that appear in all events, a table of Student Event Types which lists all interaction with the LMS outside of the Instructor Dashboard,
+and a table of Instructor Event Types of all interaction with the Instructor Dashboard in the LMS.
-The `event_source` field identifies whether the event originated in the browser (via javascript) or on the server (during the processing of a request).
-
-Server Events
+Common Fields
=============
- .. list-table::
- :widths: 20 10 10 10 50
- :header-rows: 1
-
- * - event_type
- - event fields
- - type
- - values/format
- - details
- * - `show_answer`
- - `problem_id`
- - string
- -
- - id of the problem being shown. Ex: `i4x://MITx/6.00x/problem/L15:L15_Problem_2`
- * - `save_problem_check`
- - `problem_id`
- - string
- -
- - id of the problem being shown
- * -
- - `success`
- - string
- - correct, incorrect
- - whether the problem was correct
- * -
- - `attempts`
- - integer
- - number of attempts
- -
- * -
- - `correct_map`
- - string/json
- -
- - see details below
- * -
- - `state`
- - string/json
- -
- - current problem state
- * -
- - `answers`
- - string/json
- -
- - students answers
- * -
- - `reset_problem`
- - problem_id
- - string
- - id of the problem being shown
-
-
-`correct_map` details
----------------------
-
- .. list-table::
- :widths: 15 10 15 10
- :header-rows: 1
-
- * - correct_map fields
- - type
- - values/format
- - null allowed?
- * - hint
- - string
- -
- -
- * - hintmode
- - boolean
- -
- - yes
- * - correctness
- - string
- - correct, incorrect
- -
- * - npoints
- - integer
- -
- - yes
- * - msg
- - string
- -
- -
- * - queuestate
- - string/json
- - keys: key, time
- -
-
-
-Browser Events
-==============
-
- .. list-table::
- :widths: 10 10 8 12 20 10
- :header-rows: 1
-
- * - event_type
- - fields
- - type
- - values/format
- - details
- - example
- * - `book`
- - `type`
- - string
- - `gotopage`
- -
- -
- * -
- - `old`
- - integer
- - `$PAGE`
- - from page number
- - `2`
- * -
- - `new`
- - integer
- - `$PAGE`
- - to page number
- - `25`
- * - `book`
- - `type`
- - string
- - `nextpage`
- -
- -
- * -
- - new
- - integer
- - `$PAGE`
- - next page number
- - `10`
- * - `page_close`
- - *empty*
- - string
- -
- - 'page' field indicates which page was being closed
- -
- * - play_video
- - `id`
- - string
- -
- - edX id of the video being watched
- - `i4x-HarvardX-PH207x-video-Simple_Random_Sample`
- * -
- - code
- - string
- -
- - youtube id of the video being watched
- - `FU3fCJNs94Y`
- * -
- - `currentTime`
- - float
- -
- - time the video was paused at, in seconds
- - `1.264`
- * -
- - `speed`
- - string
- - `0.75, 1.0, 1.25, 1.50`
- - video speed being played
- - `"1.0"`
- * - `pause_video`
- - `id`
- - string
- -
- - edX id of the video being watched
- -
- * -
- - `code`
- - string
- -
- - youtube id of the video being watched
- -
- * -
- - `currentTime`
- - float
- -
- - time the video was paused at
- -
- * -
- - `speed`
- - string
- - `0.75, 1.0, 1.25, 1.50`
- - video speed being played
- -
- * - `problem_check`
- - *none*
- - string
- -
- - event field contains the values of all input fields from the problem being checked (in the style of GET parameters (`key=value&key=value`))
- -
- * - `problem_show`
- - `problem`
- - string
- -
- - id of the problem being checked
- -
- * - `seq_goto`
- - `id`
- - string
- -
- - edX id of the sequence
- -
- * -
- - `old`
- - integer
- -
- - sequence element being jumped from
- - `3`
- * -
- - `new`
- - integer
- -
- - sequence element being jumped to
- - `5`
- * - `seq_next`
- - `id`
- - string
- -
- - edX id of the sequence
- -
- * -
- - `old`
- - integer
- -
- - sequence element being jumped from
- - `4`
- * -
- - `new`
- - integer
- -
- - sequence element being jumped to
- - `6`
- * - `rubric_select`
- - `location`
- - string
- -
- - location of the rubric's problem
- - `i4x://MITx/6.00x/problem/L15:L15_Problem_2`
- * -
- - `category`
- - integer
- -
- - category number of the rubric selection
- -
- * -
- - `value`
- - integer
- -
- - value selected within the category
- -
- * - `(oe / peer_grading / staff_grading)`
- `_show_problem`
- - `location`
- - string
- -
- - the location of the problem whose prompt we're showing
- -
- * - `(oe / peer_grading / staff_grading)`
- `_hide_problem`
- - `location`
- - string
- -
- - the location of the problem whose prompt we're hiding
- -
- * - `oe_show_full_feedback`
- - *empty*
- -
- -
- - the page where they're showing full feedback is already recorded
- -
- * - `oe_show_respond_to_feedback`
- - *empty*
- -
- -
- - the page where they're showing the feedback response form is already recorded
- -
- * - `oe_feedback_response_selected`
- - `value`
- - integer
- -
- - the value selected in the feedback response form
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+This section contains a table of fields common to all events.
+
+
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| Common Field | Details | Type | Values/Format |
++===========================+=============================================================+=============+====================================+
+| ``agent`` | Browser agent string of the user who triggered the event. | string | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``event`` | Specifics of the triggered event. | string/JSON | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``event_source`` | Specifies whether the triggered event originated in the | string | `'browser'`, `'server'`, `'task'` |
+| | browser or on the server. | | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``event_type`` | The type of event triggered. Values depend on | string | (see below) |
+| | ``event_source`` | | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``ip`` | IP address of the user who triggered the event. | string | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``page`` | Page user was visiting when the event was fired. | string | `'$URL'` |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``session`` | This key identifies the user's session. May be undefined. | string | 32 digits |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``time`` | Gives the GMT time at which the event was fired. | string | `'YYYY-MM-DDThh:mm:ss.xxxxxx'` |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+| ``username`` | The username of the user who caused the event to fire. This | string | |
+| | string is empty for anonymous events (i.e., user not logged | | |
+| | in). | | |
++---------------------------+-------------------------------------------------------------+-------------+------------------------------------+
+
+Event Types
+===========
+
+There are two tables of event types -- one for student events, and one for instructor events.
+Table columns describe what each event type represents, which component it originates from, what scripting language was used to fire the event, and what ``event`` fields are associated with it.
+The ``event_source`` field from the "Common Fields" table above distinguishes between events that originated in the browser (in javascript) and events that originated on the server (during the processing of a request).
+
+Event types with several different historical names are enumerated by forward slashes.
+Rows identical after the second column have been combined, with the corresponding event types enumerated by commas.
+
+
+
+Student Event Types
+-------------------
+
+The Student Event Type table lists the event types logged for interaction with the LMS outside the Instructor Dashboard.
+
+
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| Event Type | Description | Component | Language | ``event`` Fields | Type | Details |
++===================================+===============================+=====================+=================+=====================+===============+=====================================================================+
+| ``seq_goto`` | Fired when a user jumps | Sequence | CoffeeScript/JS | ``old`` | integer | Index of the unit being jumped from. |
+| | between units in | | +---------------------+---------------+---------------------------------------------------------------------+
+| | a sequence. | | | ``new`` | integer | Index of the unit being jumped to. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``id`` | integer | edX ID of the sequence. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``seq_next`` | Fired when a user navigates | Sequence | CoffeeScript/JS | ``old`` | integer | Index of the unit being navigated |
+| | to the next unit in a | | | | | away from. |
+| | sequence. | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new`` | integer | Index of the unit being navigated to. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``id`` | integer | edX ID of the sequence. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``seq_prev`` | Fired when a user navigates | Sequence | CoffeeScript/JS | ``old`` | integer | Index of the unit being navigated away |
+| | to the previous unit in a | | | | | from. |
+| | sequence. | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new`` | integer | Index of the unit being navigated to. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``id`` | integer | edX ID of the sequence. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_check`` | Fired when a user wants to | Capa | CoffeeScript/JS | | | The ``event`` field contains the |
+| | check a problem. | | | | | values of all input fields from the problem |
+| | | | | | | being checked, styled as GET parameters. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_reset`` | Fired when a problem is | Capa | CoffeeScript/JS | | | |
+| | reset. | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_show`` | Fired when a problem is | Capa | CoffeeScript/JS | ``problem`` | string | ID of the problem being shown (e.g., |
+| | shown. | | | | | i4x://MITx/6.00x/problem/L15:L15_Problem_2). |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_save`` | Fired when a problem is | Capa | CoffeeScript/JS | | | |
+| | saved. | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``oe_hide_question`` / | | Combined Open-Ended | CoffeeScript/JS | ``location`` | string | The location of the question whose prompt is |
+| ``oe_hide_problem`` | | | | | | being hidden. |
+| ``peer_grading_hide_question`` / | | Peer Grading | | | | |
+| ``peer_grading_hide_problem`` | | | | | | |
+| ``staff_grading_hide_question`` / | | Staff Grading | | | | |
+| ``staff_grading_hide_problem`` | | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``oe_show_question`` / | | Combined Open-Ended | CoffeeScript/JS | ``location`` | string | The location of the question whose prompt is |
+| ``oe_show_problem`` | | | | | | being shown. |
+| ``peer_grading_show_question`` / | | Peer Grading | | | | |
+| ``peer_grading_show_problem`` | | | | | | |
+| ``staff_grading_show_question`` / | | Staff Grading | | | | |
+| ``staff_grading_show_problem`` | | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``rubric_select`` | | Combined Open-Ended | CoffeeScript/JS | ``location`` | string | The location of the question whose rubric is |
+| | | | | | | being selected. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``selection`` | integer | Value selected on rubric. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``category`` | integer | Rubric category selected. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``oe_show_full_feedback`` | | Combined Open-Ended | CoffeeScript/JS | | | |
+| ``oe_show_respond_to_feedback`` | | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``oe_feedback_response_selected`` | | Combined Open-Ended | CoffeeScript/JS | ``value`` | integer | Value selected in the feedback response form. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``page_close`` | This event type originates | Logger | CoffeeScript/JS | | | |
+| | from within the Logger | | | | | |
+| | itself. | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``play_video`` | Fired on video play. | Video | CoffeeScript/JS | ``id`` | string | EdX ID of the video being watched (e.g., |
+| | | | | | | i4x-HarvardX-PH207x-video-Simple_Random_Sample). |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``code`` | string | YouTube ID of the video being watched (e.g., |
++-----------------------------------+-------------------------------+ | | | | FU3fCJNs94Y). |
+| ``pause_video`` | Fired on video pause. | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``currentTime`` | float | Time the video was played at, in seconds. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``speed`` | string | Video speed in use (i.e., 0.75, 1.0, 1.25, 1.50). |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``book`` | Fired when a user is reading | PDF Viewer | JS | ``type`` | string | `'gotopage'`, `'prevpage'`, `'nextpage'` |
+| | a PDF book. | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``old`` | integer | Original page number. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new`` | integer | Destination page number. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``showanswer`` / | Server-side event which | Capa Module | Python | ``problem_id`` | string | EdX ID of the problem being shown. |
+| ``show_answer`` | displays the answer to a | | | | | |
+| | problem. | | | | | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_check_fail`` | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being checked. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``answers`` | dict | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``failure`` | string | `'closed'`, `'unreset'` |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_check`` / | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| ``save_problem_check`` | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being checked. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``answers`` | dict | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``success`` | string | `'correct'`, `'incorrect'` |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``attempts`` | integer | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``correct_map`` | string / JSON | **See the table in** |
+| | | | | | | **Addendum:** ``correct_map`` **Fields and Values below** |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_rescore_fail`` | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being rescored. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``failure`` | string | `'unsupported'`, `'unanswered'`, `'input_error'`, `'unexpected'` |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``problem_rescore`` | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being rescored. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``orig_score`` | integer | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``orig_total`` | integer | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new_score`` | integer | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new_total`` | integer | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``correct_map`` | string / JSON | (See above.) |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``success`` | string | `'correct'`, `'incorrect'` |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``attempts`` | integer | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``save_problem_fail`` | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being saved. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``failure`` | string | `'closed'`, `'done'` |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``answers`` | dict | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``save_problem_success`` | | Capa Module | Python | ``state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being saved. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``answers`` | dict | |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``reset_problem_fail`` | | Capa Module | Python | ``old_state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being reset. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``failure`` | string | `'closed'`, `'not_done'` |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``reset_problem`` | | Capa Module | Python | ``old_state`` | string / JSON | Current problem state. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem_id`` | string | ID of the problem being reset. |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``new_state`` | string / JSON | New problem state. |
++-----------------------------------+-------------------------------+---------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+
+*Addendum:* ``correct_map`` *Fields and Values*
+-----------------------------------------------
+
+Table of ``correct_map`` field types and values for the ``problem_check`` student event type above.
+
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``correct_map`` **field** | **Type** | **Values / Format** | **Null Allowed?** |
++==================================================+==================================================+==================================================+==================================================+
+| ``answer_id`` | string | | |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``correctness`` | string | `'correct'`, `'incorrect'` | |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``npoints`` | integer | Points awarded for this ``answer_id``. | yes |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``msg`` | string | Gives extra message response. | |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``hint`` | string | Gives optional hint. | yes |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``hintmode`` | string | None, `'on_request'`, `'always'` | yes |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+| ``queuestate`` | dict | None when not queued, else `{key:' ', time:' '}` | yes |
+| | | where key is a secret string and time is a | |
+| | | string dump of a DateTime object of the form | |
+| | | `'%Y%m%d%H%M%S'`. | |
++--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+--------------------------------------------------+
+
+
+Instructor Event Types
+----------------------
+
+
+The Instructor Event Type table lists the event types logged for course team interaction with the Instructor Dashboard in the LMS.
+
+
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| Event Type | Description | Component | Language | ``event`` Fields | Type | Details |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``list-students``, | | Instructor Dashboard | Python | | | |
+| ``dump-grades``, | | | | | | |
+| ``dump-grades-raw``, | | | | | | |
+| ``dump-grades-csv``, | | | | | | |
+| ``dump-grades-csv-raw``, | | | | | | |
+| ``dump-answer-dist-csv``, | | | | | | |
+| ``dump-graded-assignments-config`` | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``rescore-all-submissions``, | | Instructor Dashboard | Python | ``problem`` | string | |
+| ``reset-all-attempts`` | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``course`` | string | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``delete-student-module-state``, | | Instructor Dashboard | Python | ``problem`` | string | |
+| ``rescore-student-submission`` | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``student`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``course`` | string | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``reset-student-attempts`` | | Instructor Dashboard | Python | ``old_attempts`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``student`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``problem`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``instructor`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``course`` | string | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``get-student-progress-page`` | | Instructor Dashboard | Python | ``student`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``instructor`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``course`` | string | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``list-staff``, | | Instructor Dashboard | Python | | | |
+| ``list-instructors``, | | | | | | |
+| ``list-beta-testers`` | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``add-instructor``, | | Instructor Dashboard | Python | ``instructor`` | string | |
+| ``remove-instructor`` | | | | | | |
+| | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``list-forum-admins``, | | Instructor Dashboard | Python | ``course`` | string | |
+| ``list-forum-mods``, | | | | | | |
+| ``list-forum-community-TAs`` | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``remove-forum-admin``, | | Instructor Dashboard | Python | ``username`` | string | |
+| ``add-forum-admin``, | | | | | | |
+| ``remove-forum-mod``, | | | | | | |
+| ``add-forum-mod``, | | | +---------------------+---------------+---------------------------------------------------------------------+
+| ``remove-forum-community-TA``, | | | | ``course`` | string | |
+| ``add-forum-community-TA`` | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``psychometrics-histogram-generation`` | | Instructor Dashboard | Python | ``problem`` | string | |
+| | | | | | | |
+| | | | | | | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
+| ``add-or-remove-user-group`` | | Instructor Dashboard | Python | ``event_name`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``user`` | string | |
+| | | | +---------------------+---------------+---------------------------------------------------------------------+
+| | | | | ``event`` | string | |
++----------------------------------------+-------------------------------+----------------------+-----------------+---------------------+---------------+---------------------------------------------------------------------+
From f898f7292026ab563bae6833cf705a9e4397be2e Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 24 Sep 2013 11:42:11 -0400
Subject: [PATCH 38/92] Added tags to acceptance tests to define multiple
shards.
---
.../features/advanced-settings.feature | 1 +
.../contentstore/features/checklists.feature | 1 +
.../contentstore/features/component.feature | 1 +
.../features/course-overview.feature | 1 +
.../features/course-settings.feature | 1 +
.../contentstore/features/course-team.feature | 1 +
.../features/course-updates.feature | 1 +
.../contentstore/features/courses.feature | 1 +
.../features/discussion-editor.feature | 1 +
.../contentstore/features/grading.feature | 1 +
.../contentstore/features/html-editor.feature | 1 +
.../features/problem-editor.feature | 1 +
.../contentstore/features/section.feature | 1 +
.../contentstore/features/signup.feature | 1 +
.../features/static-pages.feature | 1 +
.../contentstore/features/subsection.feature | 1 +
.../contentstore/features/textbooks.feature | 1 +
.../contentstore/features/upload.feature | 1 +
.../features/video-editor.feature | 1 +
.../contentstore/features/video.feature | 1 +
.../courseware/features/certificates.feature | 1 +
.../courseware/features/help.feature | 1 +
.../features/high-level-tabs.feature | 1 +
.../courseware/features/homepage.feature | 1 +
.../courseware/features/login.feature | 1 +
.../courseware/features/lti.feature | 3 +-
.../courseware/features/navigation.feature | 1 +
.../courseware/features/problems.feature | 1 +
.../courseware/features/registration.feature | 7 +--
.../courseware/features/signup.feature | 1 +
.../courseware/features/video.feature | 53 ++++++++++---------
.../courseware/features/word_cloud.feature | 21 ++++----
32 files changed, 72 insertions(+), 40 deletions(-)
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index b2941ac7a5e5..4af1e69ff47f 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key /value pairs
diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature
index 6289df9cfccb..f6e1fbff551a 100644
--- a/cms/djangoapps/contentstore/features/checklists.feature
+++ b/cms/djangoapps/contentstore/features/checklists.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Course checklists
Scenario: A course author sees checklists defined by edX
diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature
index a30ce96ae62c..dc2eceeead02 100644
--- a/cms/djangoapps/contentstore/features/component.feature
+++ b/cms/djangoapps/contentstore/features/component.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Component Adding
As a course author, I want to be able to add a wide variety of components
diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature
index 2cbb22ddd70a..77848ae3e08f 100644
--- a/cms/djangoapps/contentstore/features/course-overview.feature
+++ b/cms/djangoapps/contentstore/features/course-overview.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Course Overview
In order to quickly view the details of a course's section and set release dates and grading
As a course author
diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature
index be457de959f3..be19dc0f1276 100644
--- a/cms/djangoapps/contentstore/features/course-settings.feature
+++ b/cms/djangoapps/contentstore/features/course-settings.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature
index de5bb6556a29..b5415d852eda 100644
--- a/cms/djangoapps/contentstore/features/course-team.feature
+++ b/cms/djangoapps/contentstore/features/course-team.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Course Team
As a course author, I want to be able to add others to my team
diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature
index bc73479c5fb7..ce9a44024836 100644
--- a/cms/djangoapps/contentstore/features/course-updates.feature
+++ b/cms/djangoapps/contentstore/features/course-updates.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Course updates
As a course author, I want to be able to provide updates to my students
diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature
index e762d0b8a6db..686c08188adf 100644
--- a/cms/djangoapps/contentstore/features/courses.feature
+++ b/cms/djangoapps/contentstore/features/courses.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Create Course
In order offer a course on the edX platform
As a course author
diff --git a/cms/djangoapps/contentstore/features/discussion-editor.feature b/cms/djangoapps/contentstore/features/discussion-editor.feature
index e4b1f5450bab..119334253314 100644
--- a/cms/djangoapps/contentstore/features/discussion-editor.feature
+++ b/cms/djangoapps/contentstore/features/discussion-editor.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Discussion Component Editor
As a course author, I want to be able to create discussion components.
diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature
index d741af42e26a..2876186f74e9 100644
--- a/cms/djangoapps/contentstore/features/grading.feature
+++ b/cms/djangoapps/contentstore/features/grading.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Course Grading
As a course author, I want to be able to configure how my course is graded
diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature
index 4419d6018b73..a242c01ffa86 100644
--- a/cms/djangoapps/contentstore/features/html-editor.feature
+++ b/cms/djangoapps/contentstore/features/html-editor.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: HTML Editor
As a course author, I want to be able to create HTML blocks.
diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature
index 1296acec1c30..2945afecec5a 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.feature
+++ b/cms/djangoapps/contentstore/features/problem-editor.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature
index 6402db1bcb94..deaa2de6bd87 100644
--- a/cms/djangoapps/contentstore/features/section.feature
+++ b/cms/djangoapps/contentstore/features/section.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Create Section
In order offer a course on the edX platform
As a course author
diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature
index c249ad61e810..6da7c55d7094 100644
--- a/cms/djangoapps/contentstore/features/signup.feature
+++ b/cms/djangoapps/contentstore/features/signup.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Sign in
In order to use the edX content
As a new user
diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature
index 525c0cdb83ed..b193f813620a 100644
--- a/cms/djangoapps/contentstore/features/static-pages.feature
+++ b/cms/djangoapps/contentstore/features/static-pages.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Static Pages
As a course author, I want to be able to add static pages
diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature
index 6703c60c3be6..82d3f1e4250d 100644
--- a/cms/djangoapps/contentstore/features/subsection.feature
+++ b/cms/djangoapps/contentstore/features/subsection.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Create Subsection
In order offer a course on the edX platform
As a course author
diff --git a/cms/djangoapps/contentstore/features/textbooks.feature b/cms/djangoapps/contentstore/features/textbooks.feature
index 36de10daa172..4215d05de711 100644
--- a/cms/djangoapps/contentstore/features/textbooks.feature
+++ b/cms/djangoapps/contentstore/features/textbooks.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Textbooks
Scenario: No textbooks
diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature
index 1a2e25fcd35d..a0adf6b403d7 100644
--- a/cms/djangoapps/contentstore/features/upload.feature
+++ b/cms/djangoapps/contentstore/features/upload.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Upload Files
As a course author, I want to be able to upload files for my students
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature
index 966504693979..e8e2a26c9c92 100644
--- a/cms/djangoapps/contentstore/features/video-editor.feature
+++ b/cms/djangoapps/contentstore/features/video-editor.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Video Component Editor
As a course author, I want to be able to create video components.
diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature
index a6a4cec28b85..da1de109d218 100644
--- a/cms/djangoapps/contentstore/features/video.feature
+++ b/cms/djangoapps/contentstore/features/video.feature
@@ -1,3 +1,4 @@
+@shard_3
Feature: Video Component
As a course author, I want to be able to view my created videos in Studio.
diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature
index fc56a10358d1..ec7c5474ad60 100644
--- a/lms/djangoapps/courseware/features/certificates.feature
+++ b/lms/djangoapps/courseware/features/certificates.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Verified certificates
As a student,
In order to earn a verified certificate
diff --git a/lms/djangoapps/courseware/features/help.feature b/lms/djangoapps/courseware/features/help.feature
index 61305b7cfa9a..50e311e0f7e6 100644
--- a/lms/djangoapps/courseware/features/help.feature
+++ b/lms/djangoapps/courseware/features/help.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: The help module should work
In order to get help
As a student
diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature
index adbe5ec8a367..ea6f1c90b76b 100644
--- a/lms/djangoapps/courseware/features/high-level-tabs.feature
+++ b/lms/djangoapps/courseware/features/high-level-tabs.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: All the high level tabs should work
In order to preview the courseware
As a student
diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature
index 140f1f8b5fda..6c1baf405177 100644
--- a/lms/djangoapps/courseware/features/homepage.feature
+++ b/lms/djangoapps/courseware/features/homepage.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Homepage for web users
In order to get an idea what edX is about
As a an anonymous web user
diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature
index 4165a9bb9f0f..983050444984 100644
--- a/lms/djangoapps/courseware/features/login.feature
+++ b/lms/djangoapps/courseware/features/login.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Login in as a registered user
As a registered user
In order to access my content
diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature
index abdcfdb70460..7d4fd56c0104 100644
--- a/lms/djangoapps/courseware/features/lti.feature
+++ b/lms/djangoapps/courseware/features/lti.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: LTI component
As a student, I want to view LTI component in LMS.
@@ -14,4 +15,4 @@ Feature: LTI component
Scenario: LTI component in LMS is rendered incorrectly
Given the course has incorrect LTI credentials
And the course has an LTI component filled with correct fields
- Then I view the LTI but incorrect_signature warning is rendered
\ No newline at end of file
+ Then I view the LTI but incorrect_signature warning is rendered
diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature
index 8fd8b54c1ad1..20a95648472d 100644
--- a/lms/djangoapps/courseware/features/navigation.feature
+++ b/lms/djangoapps/courseware/features/navigation.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Navigate Course
As a student in an edX course
In order to view the course properly
diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature
index ad70e5a062e1..e307f617d00e 100644
--- a/lms/djangoapps/courseware/features/problems.feature
+++ b/lms/djangoapps/courseware/features/problems.feature
@@ -1,3 +1,4 @@
+@shard_1
Feature: Answer problems
As a student in an edX course
In order to test my understanding of the material
diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature
index b8115b52c543..54df31ae7d50 100644
--- a/lms/djangoapps/courseware/features/registration.feature
+++ b/lms/djangoapps/courseware/features/registration.feature
@@ -1,16 +1,17 @@
+@shard_1
Feature: Register for a course
As a registered user
In order to access my class content
I want to register for a class on the edX website
- Scenario: I can register for a course
+ Scenario: I can register for a course
Given The course "6.002x" exists
And I am logged in
And I visit the courses page
When I register for the course "6.002x"
- Then I should see the course numbered "6.002x" in my dashboard
+ Then I should see the course numbered "6.002x" in my dashboard
- Scenario: I can unregister for a course
+ Scenario: I can unregister for a course
Given I am registered for the course "6.002x"
And I visit the dashboard
Then I should see the course numbered "6.002x" in my dashboard
diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature
index 3c9f491f7d60..ae6a2c6c5f15 100644
--- a/lms/djangoapps/courseware/features/signup.feature
+++ b/lms/djangoapps/courseware/features/signup.feature
@@ -1,3 +1,4 @@
+@shard_2
Feature: Sign in
In order to use the edX content
As a new user
diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature
index cf30c55cbf93..824a86806e33 100644
--- a/lms/djangoapps/courseware/features/video.feature
+++ b/lms/djangoapps/courseware/features/video.feature
@@ -1,39 +1,40 @@
+@shard_2
Feature: Video component
As a student, I want to view course videos in LMS.
Scenario: Video component is fully rendered in the LMS in HTML5 mode
- Given the course has a Video component in HTML5 mode
- Then when I view the video it has rendered in HTML5 mode
- And all sources are correct
+ Given the course has a Video component in HTML5 mode
+ Then when I view the video it has rendered in HTML5 mode
+ And all sources are correct
# Firefox doesn't have HTML5 (only mp4 - fix here)
@skip_firefox
Scenario: Autoplay is disabled in LMS for a Video component
- Given the course has a Video component in HTML5 mode
- Then when I view the video it does not have autoplay enabled
+ Given the course has a Video component in HTML5 mode
+ Then when I view the video it does not have autoplay enabled
-# Youtube testing
-Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
-Given youtube server is up and response time is 0.4 seconds
-And the course has a Video component in Youtube_HTML5 mode
-Then when I view the video it has rendered in Youtube mode
+ # Youtube testing
+ Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
+ Given youtube server is up and response time is 0.4 seconds
+ And the course has a Video component in Youtube_HTML5 mode
+ Then when I view the video it has rendered in Youtube mode
-Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources
-Given youtube server is up and response time is 2 seconds
-And the course has a Video component in Youtube_HTML5 mode
-Then when I view the video it has rendered in HTML5 mode
+ Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources
+ Given youtube server is up and response time is 2 seconds
+ And the course has a Video component in Youtube_HTML5 mode
+ Then when I view the video it has rendered in HTML5 mode
-Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
-Given youtube server is up and response time is 2 seconds
-And the course has a Video component in Youtube mode
-Then when I view the video it has rendered in Youtube mode
+ Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
+ Given youtube server is up and response time is 2 seconds
+ And the course has a Video component in Youtube mode
+ Then when I view the video it has rendered in Youtube mode
-Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser
-Given youtube server is up and response time is 2 seconds
-And the course has a Video component in Youtube_HTML5_Unsupported_Video mode
-Then when I view the video it has rendered in Youtube mode
+ Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser
+ Given youtube server is up and response time is 2 seconds
+ And the course has a Video component in Youtube_HTML5_Unsupported_Video mode
+ Then when I view the video it has rendered in Youtube mode
-Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser
-Given the course has a Video component in HTML5_Unsupported_Video mode
-Then error message is shown
-And error message has correct text
+ Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser
+ Given the course has a Video component in HTML5_Unsupported_Video mode
+ Then error message is shown
+ And error message has correct text
diff --git a/lms/djangoapps/courseware/features/word_cloud.feature b/lms/djangoapps/courseware/features/word_cloud.feature
index 0d5baa46e18b..761fa64c0012 100644
--- a/lms/djangoapps/courseware/features/word_cloud.feature
+++ b/lms/djangoapps/courseware/features/word_cloud.feature
@@ -1,15 +1,16 @@
+@shard_2
Feature: World Cloud component
- As a student, I want to view Word Cloud component in LMS.
+ As a student, I want to view Word Cloud component in LMS.
Scenario: Word Cloud component in LMS is rendered with empty result
- Given the course has a Word Cloud component
- Then I view the word cloud and it has rendered
- When I press the Save button
- Then I see the empty result
+ Given the course has a Word Cloud component
+ Then I view the word cloud and it has rendered
+ When I press the Save button
+ Then I see the empty result
Scenario: Word Cloud component in LMS is rendered with result
- Given the course has a Word Cloud component
- Then I view the word cloud and it has rendered
- When I fill inputs
- And I press the Save button
- Then I see the result with words count
\ No newline at end of file
+ Given the course has a Word Cloud component
+ Then I view the word cloud and it has rendered
+ When I fill inputs
+ And I press the Save button
+ Then I see the result with words count
From 6ebb753823382da2ff79e6de80f199a15a7f2886 Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Thu, 19 Sep 2013 14:40:27 -0400
Subject: [PATCH 39/92] Write xunit reports for acceptance tests Refactor rake
tasks for acceptance tests Address PR comments
---
docs/internal/testing.md | 15 +++++---
jenkins/test_acceptance.sh | 4 +-
rakelib/acceptance_test.rake | 71 ++++++++++++++++++++++++++++++++++++
rakelib/deprecated.rake | 3 ++
rakelib/tests.rake | 54 ---------------------------
5 files changed, 84 insertions(+), 63 deletions(-)
create mode 100644 rakelib/acceptance_test.rake
diff --git a/docs/internal/testing.md b/docs/internal/testing.md
index 5404c16d056d..5d6c75bddb98 100644
--- a/docs/internal/testing.md
+++ b/docs/internal/testing.md
@@ -205,24 +205,27 @@ with Chrome (not Chromium) version 28.0.1500.71 with ChromeDriver
version 2.1.210398.
To run all the acceptance tests:
+ rake test:acceptance
- rake test_acceptance_lms
- rake test_acceptance_cms
+To run only for lms or cms:
+
+ rake test:acceptance:lms
+ rake test:acceptance:cms
To test only a specific feature:
- rake test_acceptance_lms["lms/djangoapps/courseware/features/problems.feature"]
+ rake test:acceptance:lms["lms/djangoapps/courseware/features/problems.feature"]
To test only a specific scenario
- rake test_acceptance_lms["lms/djangoapps/courseware/features/problems.feature -s 3"]
+ rake test:acceptance:lms["lms/djangoapps/courseware/features/problems.feature -s 3"]
To start the debugger on failure, add the `--pdb` option:
- rake test_acceptance_lms["lms/djangoapps/courseware/features/problems.feature --pdb"]
+ rake test:acceptance:lms["lms/djangoapps/courseware/features/problems.feature --pdb"]
To run tests faster by not collecting static files, you can use
-`rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`.
+`rake test:acceptance:lms:fast` and `rake test:acceptance:cms:fast`.
Acceptance tests will run on a randomized port and can be run in the background of rake cms and lms or unit tests.
To specify the port, change the LETTUCE_SERVER_PORT constant in cms/envs/acceptance.py and lms/envs/acceptance.py
diff --git a/jenkins/test_acceptance.sh b/jenkins/test_acceptance.sh
index 80705762312a..47da592863cd 100755
--- a/jenkins/test_acceptance.sh
+++ b/jenkins/test_acceptance.sh
@@ -44,8 +44,6 @@ if [ "$LETTUCE_SELENIUM_CLIENT" == saucelabs ]; then
fi
# Run the lms and cms acceptance tests
-# (the -v flag turns off color in the output)
-rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
-rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
+rake test:acceptance["$SKIP_TESTS"] || TESTS_FAILED=1
[ $TESTS_FAILED == '0' ]
diff --git a/rakelib/acceptance_test.rake b/rakelib/acceptance_test.rake
new file mode 100644
index 000000000000..111ba986151d
--- /dev/null
+++ b/rakelib/acceptance_test.rake
@@ -0,0 +1,71 @@
+ACCEPTANCE_DB = 'test_root/db/test_edx.db'
+ACCEPTANCE_REPORT_DIR = report_dir_path('acceptance')
+directory ACCEPTANCE_REPORT_DIR
+
+def run_acceptance_tests(system, harvest_args)
+ # Create the acceptance report directory
+ # because if it doesn't exist then lettuce will give an IOError.
+ report_dir = report_dir_path('acceptance')
+
+ report_file = File.join(ACCEPTANCE_REPORT_DIR, "#{system}.xml")
+ report_args = "--with-xunit --xunit-file #{report_file}"
+ test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--verbosity 2', '--tag -skip', report_args, harvest_args))
+end
+
+task :setup_acceptance_db do
+ # HACK: Since the CMS depends on the existence of some database tables
+ # that are now in common but used to be in LMS (Role/Permissions for Forums)
+ # we need to create/migrate the database tables defined in the LMS.
+ # We might be able to address this by moving out the migrations from
+ # lms/django_comment_client, but then we'd have to repair all the existing
+ # migrations from the upgrade tables in the DB.
+ # But for now for either system (lms or cms), use the lms
+ # definitions to sync and migrate.
+ if File.exists?(ACCEPTANCE_DB)
+ File.delete(ACCEPTANCE_DB)
+ end
+
+ sh(django_admin('lms', 'acceptance', 'syncdb', '--noinput'))
+ sh(django_admin('lms', 'acceptance', 'migrate', '--noinput'))
+end
+
+task :prep_for_acceptance_tests => [
+ :clean_reports_dir, :clean_test_files, ACCEPTANCE_REPORT_DIR,
+ :install_prereqs, :setup_acceptance_db
+]
+
+namespace :test do
+ namespace :acceptance do
+ task :all, [:harvest_args] => [
+ :prep_for_acceptance_tests,
+ "^^lms:gather_assets:acceptance",
+ "^^cms:gather_assets:acceptance"
+ ] do |t, args|
+ run_acceptance_tests('lms', args.harvest_args)
+ run_acceptance_tests('cms', args.harvest_args)
+ end
+
+ ['lms', 'cms'].each do |system|
+ desc "Run the acceptance tests for the #{system}"
+ task system, [:harvest_args] => [
+ :prep_for_acceptance_tests,
+ "^^#{system}:gather_assets:acceptance"
+ ] do |t, args|
+ args.with_defaults(:harvest_args => '')
+ run_acceptance_tests(system, args.harvest_args)
+ end
+
+ desc "Run acceptance tests for the #{system} without collectstatic or db migrations"
+ task "#{system}:fast", [:harvest_args] => [
+ :clean_reports_dir, ACCEPTANCE_REPORT_DIR,
+ ] do |t, args|
+ args.with_defaults(:harvest_args => '')
+ run_acceptance_tests(system, args.harvest_args)
+ end
+ end
+ end
+ desc "Run the lettuce acceptance tests for lms and cms"
+ task :acceptance, [:harvest_args] do |t, args|
+ Rake::Task["test:acceptance:all"].invoke(args.harvest_args)
+ end
+end
diff --git a/rakelib/deprecated.rake b/rakelib/deprecated.rake
index 55c033226cf4..fc7d67e43c19 100644
--- a/rakelib/deprecated.rake
+++ b/rakelib/deprecated.rake
@@ -24,6 +24,8 @@ end
deprecated("jasmine:#{system}:phantomjs", "test:js:run", system)
deprecated("#{system}:check_settings:jasmine", "")
deprecated("#{system}:gather_assets:jasmine", "")
+ deprecated("test_acceptance_#{system}", "test:acceptance:#{system}")
+ deprecated("fasttest_acceptance_#{system}", "test:acceptance:#{system}:fast")
end
Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
@@ -49,3 +51,4 @@ deprecated("jasmine:common/static/coffee:phantomjs", "test:js:run", "common")
deprecated("jasmine", "test:js")
deprecated("jasmine:phantomjs", "test:js:run")
deprecated("jasmine:browser", "test:js:dev")
+deprecated("test_acceptance", "test:acceptance")
diff --git a/rakelib/tests.rake b/rakelib/tests.rake
index 04a32961d721..fb6cf575bdd0 100644
--- a/rakelib/tests.rake
+++ b/rakelib/tests.rake
@@ -4,8 +4,6 @@ CLOBBER.include(REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
# Create the directory to hold coverage reports, if it doesn't already exist.
directory REPORT_DIR
-ACCEPTANCE_DB = 'test_root/db/test_edx.db'
-
def test_id_dir(path)
return File.join(".testids", path.to_s)
end
@@ -32,38 +30,6 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
test_sh(run_under_coverage(cmd, system))
end
-def create_acceptance_db(system)
- # HACK: Since now the CMS depends on the existence of some database tables
- # that used to be in LMS (Role/Permissions for Forums) we need to make
- # sure the acceptance tests create/migrate the database tables
- # that are represented in the LMS. We might be able to address this by moving
- # out the migrations from lms/django_comment_client, but then we'd have to
- # repair all the existing migrations from the upgrade tables in the DB.
- if system == :cms
- sh(django_admin('lms', 'acceptance', 'syncdb', '--noinput'))
- sh(django_admin('lms', 'acceptance', 'migrate', '--noinput'))
- end
- sh(django_admin(system, 'acceptance', 'syncdb', '--noinput'))
- sh(django_admin(system, 'acceptance', 'migrate', '--noinput'))
-end
-
-def setup_acceptance_db(system, fasttest=false)
- # If running under fasttest mode and the database already
- # exists, skip the migrations.
- if File.exists?(ACCEPTANCE_DB)
- if not fasttest
- File.delete(ACCEPTANCE_DB)
- create_acceptance_db(system)
- end
- else
- create_acceptance_db(system)
- end
-end
-
-def run_acceptance_tests(system, report_dir, harvest_args)
- test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--verbosity 2', '--tag -skip', harvest_args))
-end
-
# Run documentation tests
desc "Run documentation tests"
task :test_docs do
@@ -110,26 +76,6 @@ TEST_TASK_DIRS = []
run_tests(system, report_dir, args.test_id)
end
- # Run acceptance tests
- desc "Run acceptance tests"
- task "test_acceptance_#{system}", [:harvest_args] => [
- :clean_test_files, :install_prereqs,
- "#{system}:gather_assets:acceptance"
- ] do |t, args|
- setup_acceptance_db(system)
- Rake::Task["fasttest_acceptance_#{system}"].invoke(args.harvest_args)
- end
-
- desc "Run acceptance tests without collectstatic or database migrations"
- task "fasttest_acceptance_#{system}", [:harvest_args] => [
- report_dir, :clean_reports_dir
- ] do |t, args|
- args.with_defaults(:harvest_args => '')
- setup_acceptance_db(system, fasttest=true)
- run_acceptance_tests(system, report_dir, args.harvest_args)
- end
-
-
task :fasttest => "fasttest_#{system}"
TEST_TASK_DIRS << system
From 7669b4d7774bcb0efee03ad6e47218e6738a744c Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Tue, 24 Sep 2013 14:21:56 -0400
Subject: [PATCH 40/92] Namespace the acceptance tests so they show up nicely
in jenkins
Conflicts:
cms/djangoapps/contentstore/features/advanced-settings.feature
cms/djangoapps/contentstore/features/checklists.feature
cms/djangoapps/contentstore/features/component.feature
cms/djangoapps/contentstore/features/course-overview.feature
cms/djangoapps/contentstore/features/course-settings.feature
cms/djangoapps/contentstore/features/course-team.feature
cms/djangoapps/contentstore/features/course-updates.feature
cms/djangoapps/contentstore/features/courses.feature
cms/djangoapps/contentstore/features/discussion-editor.feature
cms/djangoapps/contentstore/features/grading.feature
cms/djangoapps/contentstore/features/html-editor.feature
cms/djangoapps/contentstore/features/problem-editor.feature
cms/djangoapps/contentstore/features/section.feature
cms/djangoapps/contentstore/features/signup.feature
cms/djangoapps/contentstore/features/static-pages.feature
cms/djangoapps/contentstore/features/subsection.feature
cms/djangoapps/contentstore/features/textbooks.feature
cms/djangoapps/contentstore/features/upload.feature
cms/djangoapps/contentstore/features/video-editor.feature
cms/djangoapps/contentstore/features/video.feature
lms/djangoapps/courseware/features/certificates.feature
lms/djangoapps/courseware/features/help.feature
lms/djangoapps/courseware/features/high-level-tabs.feature
lms/djangoapps/courseware/features/homepage.feature
lms/djangoapps/courseware/features/login.feature
lms/djangoapps/courseware/features/lti.feature
lms/djangoapps/courseware/features/navigation.feature
lms/djangoapps/courseware/features/problems.feature
lms/djangoapps/courseware/features/registration.feature
lms/djangoapps/courseware/features/signup.feature
lms/djangoapps/courseware/features/video.feature
lms/djangoapps/courseware/features/word_cloud.feature
---
.../contentstore/features/advanced-settings.feature | 2 +-
cms/djangoapps/contentstore/features/checklists.feature | 2 +-
cms/djangoapps/contentstore/features/component.feature | 2 +-
cms/djangoapps/contentstore/features/course-overview.feature | 2 +-
cms/djangoapps/contentstore/features/course-settings.feature | 2 +-
cms/djangoapps/contentstore/features/course-team.feature | 2 +-
cms/djangoapps/contentstore/features/course-updates.feature | 2 +-
cms/djangoapps/contentstore/features/courses.feature | 2 +-
.../contentstore/features/discussion-editor.feature | 2 +-
cms/djangoapps/contentstore/features/grading.feature | 2 +-
cms/djangoapps/contentstore/features/html-editor.feature | 2 +-
cms/djangoapps/contentstore/features/problem-editor.feature | 2 +-
cms/djangoapps/contentstore/features/section.feature | 2 +-
cms/djangoapps/contentstore/features/signup.feature | 2 +-
cms/djangoapps/contentstore/features/static-pages.feature | 2 +-
cms/djangoapps/contentstore/features/subsection.feature | 2 +-
cms/djangoapps/contentstore/features/textbooks.feature | 2 +-
cms/djangoapps/contentstore/features/upload.feature | 2 +-
cms/djangoapps/contentstore/features/video-editor.feature | 2 +-
cms/djangoapps/contentstore/features/video.feature | 2 +-
lms/djangoapps/courseware/features/certificates.feature | 2 +-
lms/djangoapps/courseware/features/help.feature | 2 +-
lms/djangoapps/courseware/features/high-level-tabs.feature | 2 +-
lms/djangoapps/courseware/features/homepage.feature | 2 +-
lms/djangoapps/courseware/features/login.feature | 2 +-
lms/djangoapps/courseware/features/lti.feature | 2 +-
lms/djangoapps/courseware/features/navigation.feature | 2 +-
lms/djangoapps/courseware/features/openended.feature | 2 +-
lms/djangoapps/courseware/features/problems.feature | 2 +-
lms/djangoapps/courseware/features/registration.feature | 2 +-
lms/djangoapps/courseware/features/signup.feature | 2 +-
lms/djangoapps/courseware/features/video.feature | 2 +-
lms/djangoapps/courseware/features/word_cloud.feature | 4 ++--
33 files changed, 34 insertions(+), 34 deletions(-)
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index 4af1e69ff47f..03e8e6ea236f 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Advanced (manual) course policy
+Feature: CMS.Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key /value pairs
diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature
index f6e1fbff551a..23995f5aaa9e 100644
--- a/cms/djangoapps/contentstore/features/checklists.feature
+++ b/cms/djangoapps/contentstore/features/checklists.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Course checklists
+Feature: CMS.Course checklists
Scenario: A course author sees checklists defined by edX
Given I have opened a new course in Studio
diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature
index dc2eceeead02..9440ce7dc2f3 100644
--- a/cms/djangoapps/contentstore/features/component.feature
+++ b/cms/djangoapps/contentstore/features/component.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Component Adding
+Feature: CMS.Component Adding
As a course author, I want to be able to add a wide variety of components
@skip
diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature
index 77848ae3e08f..858572d75e27 100644
--- a/cms/djangoapps/contentstore/features/course-overview.feature
+++ b/cms/djangoapps/contentstore/features/course-overview.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Course Overview
+Feature: CMS.Course Overview
In order to quickly view the details of a course's section and set release dates and grading
As a course author
I want to use the course overview page
diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature
index be19dc0f1276..230b1a04ad7d 100644
--- a/cms/djangoapps/contentstore/features/course-settings.feature
+++ b/cms/djangoapps/contentstore/features/course-settings.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Course Settings
+Feature: CMS.Course Settings
As a course author, I want to be able to configure my course settings.
# Safari has trouble keeps dates on refresh
diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature
index b5415d852eda..05a59002f51c 100644
--- a/cms/djangoapps/contentstore/features/course-team.feature
+++ b/cms/djangoapps/contentstore/features/course-team.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Course Team
+Feature: CMS.Course Team
As a course author, I want to be able to add others to my team
Scenario: Admins can add other users
diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature
index ce9a44024836..15257bd911ab 100644
--- a/cms/djangoapps/contentstore/features/course-updates.feature
+++ b/cms/djangoapps/contentstore/features/course-updates.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Course updates
+Feature: CMS.Course updates
As a course author, I want to be able to provide updates to my students
# Internet explorer can't select all so the update appears weirdly
diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature
index 686c08188adf..c5316e0d6c40 100644
--- a/cms/djangoapps/contentstore/features/courses.feature
+++ b/cms/djangoapps/contentstore/features/courses.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Create Course
+Feature: CMS.Create Course
In order offer a course on the edX platform
As a course author
I want to create courses
diff --git a/cms/djangoapps/contentstore/features/discussion-editor.feature b/cms/djangoapps/contentstore/features/discussion-editor.feature
index 119334253314..7278accf0b55 100644
--- a/cms/djangoapps/contentstore/features/discussion-editor.feature
+++ b/cms/djangoapps/contentstore/features/discussion-editor.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Discussion Component Editor
+Feature: CMS.Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature
index 2876186f74e9..f3ce1823e634 100644
--- a/cms/djangoapps/contentstore/features/grading.feature
+++ b/cms/djangoapps/contentstore/features/grading.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Course Grading
+Feature: CMS.Course Grading
As a course author, I want to be able to configure how my course is graded
Scenario: Users can add grading ranges
diff --git a/cms/djangoapps/contentstore/features/html-editor.feature b/cms/djangoapps/contentstore/features/html-editor.feature
index a242c01ffa86..29dcbbbfc512 100644
--- a/cms/djangoapps/contentstore/features/html-editor.feature
+++ b/cms/djangoapps/contentstore/features/html-editor.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: HTML Editor
+Feature: CMS.HTML Editor
As a course author, I want to be able to create HTML blocks.
Scenario: User can view metadata
diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature
index 2945afecec5a..0d95b5d78507 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.feature
+++ b/cms/djangoapps/contentstore/features/problem-editor.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Problem Editor
+Feature: CMS.Problem Editor
As a course author, I want to be able to create problems and edit their settings.
Scenario: User can view metadata
diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature
index deaa2de6bd87..16d833aed873 100644
--- a/cms/djangoapps/contentstore/features/section.feature
+++ b/cms/djangoapps/contentstore/features/section.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Create Section
+Feature: CMS.Create Section
In order offer a course on the edX platform
As a course author
I want to create and edit sections
diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature
index 6da7c55d7094..f1f2c1358351 100644
--- a/cms/djangoapps/contentstore/features/signup.feature
+++ b/cms/djangoapps/contentstore/features/signup.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Sign in
+Feature: CMS.Sign in
In order to use the edX content
As a new user
I want to signup for a student account
diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature
index b193f813620a..54d23d985de2 100644
--- a/cms/djangoapps/contentstore/features/static-pages.feature
+++ b/cms/djangoapps/contentstore/features/static-pages.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Static Pages
+Feature: CMS.Static Pages
As a course author, I want to be able to add static pages
Scenario: Users can add static pages
diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature
index 82d3f1e4250d..2cb708ad3cfc 100644
--- a/cms/djangoapps/contentstore/features/subsection.feature
+++ b/cms/djangoapps/contentstore/features/subsection.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Create Subsection
+Feature: CMS.Create Subsection
In order offer a course on the edX platform
As a course author
I want to create and edit subsections
diff --git a/cms/djangoapps/contentstore/features/textbooks.feature b/cms/djangoapps/contentstore/features/textbooks.feature
index 4215d05de711..010e4902569c 100644
--- a/cms/djangoapps/contentstore/features/textbooks.feature
+++ b/cms/djangoapps/contentstore/features/textbooks.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Textbooks
+Feature: CMS.Textbooks
Scenario: No textbooks
Given I have opened a new course in Studio
diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature
index a0adf6b403d7..fed8c65ca108 100644
--- a/cms/djangoapps/contentstore/features/upload.feature
+++ b/cms/djangoapps/contentstore/features/upload.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Upload Files
+Feature: CMS.Upload Files
As a course author, I want to be able to upload files for my students
# Uploading isn't working on safari with sauce labs
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature
index e8e2a26c9c92..d5b4a2a03b4e 100644
--- a/cms/djangoapps/contentstore/features/video-editor.feature
+++ b/cms/djangoapps/contentstore/features/video-editor.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Video Component Editor
+Feature: CMS.Video Component Editor
As a course author, I want to be able to create video components.
Scenario: User can view Video metadata
diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature
index da1de109d218..ad6fea083be2 100644
--- a/cms/djangoapps/contentstore/features/video.feature
+++ b/cms/djangoapps/contentstore/features/video.feature
@@ -1,5 +1,5 @@
@shard_3
-Feature: Video Component
+Feature: CMS.Video Component
As a course author, I want to be able to view my created videos in Studio.
# Video Alpha Features will work in Firefox only when Firefox is the active window
diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature
index ec7c5474ad60..8ea68df5b156 100644
--- a/lms/djangoapps/courseware/features/certificates.feature
+++ b/lms/djangoapps/courseware/features/certificates.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Verified certificates
+Feature: LMS.Verified certificates
As a student,
In order to earn a verified certificate
I want to sign up for a verified certificate course.
diff --git a/lms/djangoapps/courseware/features/help.feature b/lms/djangoapps/courseware/features/help.feature
index 50e311e0f7e6..db8c49900a78 100644
--- a/lms/djangoapps/courseware/features/help.feature
+++ b/lms/djangoapps/courseware/features/help.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: The help module should work
+Feature: LMS.The help module should work
In order to get help
As a student
I want to be able to report a problem
diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature
index ea6f1c90b76b..8b7fdd300d82 100644
--- a/lms/djangoapps/courseware/features/high-level-tabs.feature
+++ b/lms/djangoapps/courseware/features/high-level-tabs.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: All the high level tabs should work
+Feature: LMS.All the high level tabs should work
In order to preview the courseware
As a student
I want to navigate through the high level tabs
diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature
index 6c1baf405177..fd845b765bf0 100644
--- a/lms/djangoapps/courseware/features/homepage.feature
+++ b/lms/djangoapps/courseware/features/homepage.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Homepage for web users
+Feature: LMS.Homepage for web users
In order to get an idea what edX is about
As a an anonymous web user
I want to check the information on the home page
diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature
index 983050444984..7af151ed26ed 100644
--- a/lms/djangoapps/courseware/features/login.feature
+++ b/lms/djangoapps/courseware/features/login.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Login in as a registered user
+Feature: LMS.Login in as a registered user
As a registered user
In order to access my content
I want to be able to login in to edX
diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature
index 7d4fd56c0104..a7182a3daeea 100644
--- a/lms/djangoapps/courseware/features/lti.feature
+++ b/lms/djangoapps/courseware/features/lti.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: LTI component
+Feature: LMS.LTI component
As a student, I want to view LTI component in LMS.
Scenario: LTI component in LMS is not rendered
diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature
index 20a95648472d..69a7a5a4a4e0 100644
--- a/lms/djangoapps/courseware/features/navigation.feature
+++ b/lms/djangoapps/courseware/features/navigation.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Navigate Course
+Feature: LMS.Navigate Course
As a student in an edX course
In order to view the course properly
I want to be able to navigate through the content
diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature
index 1ab496144fe3..0ebf848aeccd 100644
--- a/lms/djangoapps/courseware/features/openended.feature
+++ b/lms/djangoapps/courseware/features/openended.feature
@@ -1,4 +1,4 @@
-Feature: Open ended grading
+Feature: LMS.Open ended grading
As a student in an edX course
In order to complete the courseware questions
I want the machine learning grading to be functional
diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature
index e307f617d00e..2f15619a9496 100644
--- a/lms/djangoapps/courseware/features/problems.feature
+++ b/lms/djangoapps/courseware/features/problems.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Answer problems
+Feature: LMS.Answer problems
As a student in an edX course
In order to test my understanding of the material
I want to answer problems
diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature
index 54df31ae7d50..c779462db355 100644
--- a/lms/djangoapps/courseware/features/registration.feature
+++ b/lms/djangoapps/courseware/features/registration.feature
@@ -1,5 +1,5 @@
@shard_1
-Feature: Register for a course
+Feature: LMS.Register for a course
As a registered user
In order to access my class content
I want to register for a class on the edX website
diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature
index ae6a2c6c5f15..b04a950bef8b 100644
--- a/lms/djangoapps/courseware/features/signup.feature
+++ b/lms/djangoapps/courseware/features/signup.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Sign in
+Feature: LMS.Sign in
In order to use the edX content
As a new user
I want to signup for a student account
diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature
index 824a86806e33..001481a6a5c1 100644
--- a/lms/djangoapps/courseware/features/video.feature
+++ b/lms/djangoapps/courseware/features/video.feature
@@ -1,5 +1,5 @@
@shard_2
-Feature: Video component
+Feature: LMS.Video component
As a student, I want to view course videos in LMS.
Scenario: Video component is fully rendered in the LMS in HTML5 mode
diff --git a/lms/djangoapps/courseware/features/word_cloud.feature b/lms/djangoapps/courseware/features/word_cloud.feature
index 761fa64c0012..f80765e72bde 100644
--- a/lms/djangoapps/courseware/features/word_cloud.feature
+++ b/lms/djangoapps/courseware/features/word_cloud.feature
@@ -1,6 +1,6 @@
@shard_2
-Feature: World Cloud component
- As a student, I want to view Word Cloud component in LMS.
+Feature: LMS.World Cloud component
+ As a student, I want to view Word Cloud component in LMS.
Scenario: Word Cloud component in LMS is rendered with empty result
Given the course has a Word Cloud component
From 1a0b752a812b04b85be38bdd9feeaab4c61732c5 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Tue, 24 Sep 2013 11:17:11 -0400
Subject: [PATCH 41/92] Review fixes
---
common/djangoapps/contentserver/middleware.py | 14 +--
common/djangoapps/contentserver/tests/test.py | 110 ++++++++++--------
common/djangoapps/student/models.py | 8 +-
common/djangoapps/student/tests/tests.py | 11 ++
4 files changed, 83 insertions(+), 60 deletions(-)
diff --git a/common/djangoapps/contentserver/middleware.py b/common/djangoapps/contentserver/middleware.py
index 30ab977b9683..b9c14cd537aa 100644
--- a/common/djangoapps/contentserver/middleware.py
+++ b/common/djangoapps/contentserver/middleware.py
@@ -1,5 +1,5 @@
-from django.http import HttpResponse, HttpResponseNotModified
-from django.shortcuts import redirect
+from django.http import (HttpResponse, HttpResponseNotModified,
+ HttpResponseForbidden)
from student.models import CourseEnrollment
from xmodule.contentstore.django import contentstore
@@ -46,13 +46,11 @@ def process_request(self, request):
# Check that user has access to content
if getattr(content, "locked", False):
if not hasattr(request, "user") or not request.user.is_authenticated():
- return HttpResponse('Unauthorized', status=403)
+ return HttpResponseForbidden('Unauthorized')
course_partial_id = "/".join([loc.org, loc.course])
- if not CourseEnrollment.is_enrolled_by_partial(request.user, course_partial_id):
- return HttpResponse('Unauthorized', status=403)
-
-
- # see if the last-modified at hasn't changed, if not return a 302 (Not Modified)
+ if not request.user.is_staff and not CourseEnrollment.is_enrolled_by_partial(
+ request.user, course_partial_id):
+ return HttpResponseForbidden('Unauthorized')
# convert over the DB persistent last modified timestamp to a HTTP compatible
# timestamp, so we can simply compare the strings
diff --git a/common/djangoapps/contentserver/tests/test.py b/common/djangoapps/contentserver/tests/test.py
index 7313ba3f9b96..94a614c85ab7 100644
--- a/common/djangoapps/contentserver/tests/test.py
+++ b/common/djangoapps/contentserver/tests/test.py
@@ -9,7 +9,6 @@
from django.contrib.auth.models import User
from django.conf import settings
-from django.core.urlresolvers import reverse
from django.test.client import Client
from django.test.utils import override_settings
@@ -20,7 +19,7 @@
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import (studio_store_config,
- ModuleStoreTestCase)
+ ModuleStoreTestCase)
from xmodule.modulestore.xml_importer import import_from_xml
log = logging.getLogger(__name__)
@@ -45,28 +44,39 @@ def setUp(self):
settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
- base = "http://127.0.0.1:8000"
self.client = Client()
self.contentstore = contentstore()
- # A locked the asset
- loc = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt' )
- self.loc = loc
- rel_url = StaticContent.get_url_path_from_location(loc)
- self.url = base + rel_url
+ # A locked asset
+ self.loc_locked = Location('c4x', 'edX', 'toy', 'asset', 'sample_static.txt')
+ self.url_locked = StaticContent.get_url_path_from_location(self.loc_locked)
# An unlocked asset
- loc2 = Location('c4x', 'edX', 'toy', 'asset', 'another_static.txt' )
- self.loc2 = loc2
- rel_url2 = StaticContent.get_url_path_from_location(loc2)
- self.url2 = base + rel_url2
-
+ self.loc_unlocked = Location('c4x', 'edX', 'toy', 'asset', 'another_static.txt')
+ self.url_unlocked = StaticContent.get_url_path_from_location(self.loc_unlocked)
import_from_xml(modulestore('direct'), 'common/test/data/', ['toy'],
static_content_store=self.contentstore, verbose=True)
- self.contentstore.set_attr(self.loc, 'locked', True)
+ self.contentstore.set_attr(self.loc_locked, 'locked', True)
+ # Create user
+ self.usr = 'testuser'
+ self.pwd = 'foo'
+ email = 'test+courses@edx.org'
+ self.user = User.objects.create_user(self.usr, email, self.pwd)
+ self.user.is_active = True
+ self.user.save()
+
+ # Create staff user
+ self.staff_usr = 'stafftestuser'
+ self.staff_pwd = 'foo'
+ staff_email = 'stafftest+courses@edx.org'
+ self.staff_user = User.objects.create_user(self.staff_usr, staff_email,
+ self.staff_pwd)
+ self.staff_user.is_active = True
+ self.staff_user.is_staff = True
+ self.staff_user.save()
def tearDown(self):
@@ -77,46 +87,50 @@ def test_unlocked_asset(self):
"""
Test that unlocked assets are being served.
"""
- # Logout user
self.client.logout()
+ resp = self.client.get(self.url_unlocked)
+ self.assertEqual(resp.status_code, 200) #pylint: disable=E1103
- resp = self.client.get(self.url2)
- self.assertEqual(resp.status_code, 200)
+ def test_locked_asset_not_logged_in(self):
+ """
+ Test that locked assets behave appropriately in case the user is not
+ logged in.
+ """
+ self.client.logout()
+ resp = self.client.get(self.url_locked)
+ self.assertEqual(resp.status_code, 403) #pylint: disable=E1103
+ def test_locked_asset_not_registered(self):
+ """
+ Test that locked assets behave appropriately in case user is logged in
+ in but not registered for the course.
+ """
+ self.client.login(username=self.usr, password=self.pwd)
+ resp = self.client.get(self.url_locked)
+ self.assertEqual(resp.status_code, 403) #pylint: disable=E1103
- def test_locked_asset(self):
+ def test_locked_asset_registered(self):
"""
- Test that locked assets behave appropriately in case:
- (1) User is not logged in
- (2) User is logged in in but not registerd for the course
- (3) User is logged in and registered
+ Test that locked assets behave appropriately in case user is logged in
+ and registered for the course.
"""
+ #pylint: disable=E1101
+ course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall'])
+ CourseEnrollment.enroll(self.user, course_id)
+ self.assertTrue(CourseEnrollment.is_enrolled(self.user, course_id))
- # Case (1)
- resp = self.client.get(self.url)
- self.assertEqual(resp.status_code, 403)
+ self.client.login(username=self.usr, password=self.pwd)
+ resp = self.client.get(self.url_locked)
+ self.assertEqual(resp.status_code, 200) #pylint: disable=E1103
- # Case (2)
- # Create user and login
- uname = 'testuser'
- email = 'test+courses@edx.org'
- password = 'foo'
- user = User.objects.create_user(uname, email, password)
- user.is_active = True
- user.save()
- self.client.login(username=uname, password=password)
- log.debug("User logged in")
-
- resp = self.client.get(self.url)
- log.debug("Received response %s", resp)
- self.assertEqual(resp.status_code, 403)
-
- # Case (3)
- # Enroll student
- course_id = "/".join([self.loc.org, self.loc.course, '2012_Fall'])
- CourseEnrollment.enroll(user, course_id)
- self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
-
- resp = self.client.get(self.url)
- self.assertEqual(resp.status_code, 200)
+ def test_locked_asset_staff(self):
+ """
+ Test that locked assets behave appropriately in case user is staff.
+ """
+ #pylint: disable=E1101
+ course_id = "/".join([self.loc_locked.org, self.loc_locked.course, '2012_Fall'])
+
+ self.client.login(username=self.staff_usr, password=self.staff_pwd)
+ resp = self.client.get(self.url_locked)
+ self.assertEqual(resp.status_code, 200) #pylint: disable=E1103
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 8cda698d8b89..da16a2fda227 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -861,10 +861,10 @@ def is_enrolled_by_partial(cls, user, course_id_partial):
"""
try:
return CourseEnrollment.objects.filter(
- user=user,
- course_id__startswith=course_id_partial,
- is_active=1
- ).exists()
+ user=user,
+ course_id__startswith=course_id_partial,
+ is_active=1
+ ).exists()
except cls.DoesNotExist:
return False
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 397816ec00ab..a6466ee9f96c 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -213,23 +213,34 @@ class EnrollInCourseTest(TestCase):
def test_enrollment(self):
user = User.objects.create_user("joe", "joe@joe.com", "password")
course_id = "edX/Test101/2013"
+ course_id_partial = "edX/Test101"
# Test basic enrollment
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
+ self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
+ course_id_partial))
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
+ self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
+ course_id_partial))
# Enrolling them again should be harmless
CourseEnrollment.enroll(user, course_id)
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
+ self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user,
+ course_id_partial))
# Now unenroll the user
CourseEnrollment.unenroll(user, course_id)
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
+ self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
+ course_id_partial))
# Unenrolling them again should also be harmless
CourseEnrollment.unenroll(user, course_id)
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
+ self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user,
+ course_id_partial))
# The enrollment record should still exist, just be inactive
enrollment_record = CourseEnrollment.objects.get(
From 1b5903935e1e8c9fe030ad3d3de9f71471447b8f Mon Sep 17 00:00:00 2001
From: Nick Parlante
Date: Mon, 23 Sep 2013 12:40:41 -0700
Subject: [PATCH 42/92] Add "Download CSV of all student anonymized IDs" button
to instructor dashboard
This is a recurrent ops problem, so we wanted to make it available
on the instructor dashboard.
---
lms/djangoapps/instructor/views/legacy.py | 11 ++++++++++-
lms/templates/courseware/instructor_dashboard.html | 3 +++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py
index 2efc7e134421..df467b8637e8 100644
--- a/lms/djangoapps/instructor/views/legacy.py
+++ b/lms/djangoapps/instructor/views/legacy.py
@@ -50,7 +50,7 @@
from instructor_task.views import get_task_completion_info
from mitxmako.shortcuts import render_to_response
from psychometrics import psychoanalyze
-from student.models import CourseEnrollment, CourseEnrollmentAllowed
+from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
from student.views import course_from_id
import track.views
from mitxmako.shortcuts import render_to_string
@@ -584,6 +584,15 @@ def getdat(u):
datatable['title'] = 'Student state for problem %s' % problem_to_dump
return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable)
+ elif 'Download CSV of all student anonymized IDs' in action:
+ students = User.objects.filter(
+ courseenrollment__course_id=course_id,
+ ).order_by('id')
+
+ datatable = {'header': ['User ID', 'Anonymized user ID']}
+ datatable['data'] = [[s.id, unique_id_for_user(s)] for s in students]
+ return return_csv(course_id.replace('/', '-')+'-anon-ids.csv', datatable)
+
#----------------------------------------
# Group management
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index effa5852d8b8..7f41c82c9dea 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -416,6 +416,9 @@
${_("Student-specific grade inspection and adjustment")}
+
+
+
%endif
From d1a82f2ac5fa32de531f1e0750ec0f96ca7ba91c Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Fri, 20 Sep 2013 15:31:50 -0400
Subject: [PATCH 43/92] Import/export static content properties Even though xml
based courses will ignore.
---
common/lib/xmodule/xmodule/contentstore/mongo.py | 10 +++++++++-
.../xmodule/xmodule/modulestore/xml_exporter.py | 8 ++++++--
.../xmodule/xmodule/modulestore/xml_importer.py | 16 ++++++++++++++--
3 files changed, 29 insertions(+), 5 deletions(-)
diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py
index 40c0b4bc9f93..0322b394ca73 100644
--- a/common/lib/xmodule/xmodule/contentstore/mongo.py
+++ b/common/lib/xmodule/xmodule/contentstore/mongo.py
@@ -12,6 +12,7 @@
from xmodule.exceptions import NotFoundError
from fs.osfs import OSFS
import os
+import json
class MongoContentStore(ContentStore):
@@ -103,12 +104,19 @@ def export(self, location, output_directory):
with disk_fs.open(content.name, 'wb') as asset_file:
asset_file.write(content.data)
- def export_all_for_course(self, course_location, output_directory):
+ def export_all_for_course(self, course_location, output_directory, policy_file):
+ policy = {}
assets = self.get_all_content_for_course(course_location)
for asset in assets:
asset_location = Location(asset['_id'])
self.export(asset_location, output_directory)
+ for attr, value in asset.iteritems():
+ if attr not in ['_id', 'md5', 'uploadDate', 'length', 'chunkSize']:
+ policy.setdefault(asset_location.url(), {})[attr] = value
+
+ with open(policy_file, 'w') as f:
+ json.dump(policy, f)
def get_all_content_thumbnails_for_course(self, location):
return self._get_all_content_for_course(location, get_thumbnails=True)
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
index c9d6e96761d9..a54d188b61d9 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py
@@ -55,8 +55,13 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
+ policies_dir = export_fs.makeopendir('policies')
# export the static assets
- contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/')
+ contentstore.export_all_for_course(
+ course_location,
+ root_dir + '/' + course_dir + '/static/',
+ root_dir + '/' + course_dir + '/policies/assets.json',
+ )
# export the static tabs
export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html')
@@ -71,7 +76,6 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
export_extra_content(export_fs, modulestore, course_location, 'about', 'about', '.html')
# export the grading policy
- policies_dir = export_fs.makeopendir('policies')
course_run_policy_dir = policies_dir.makeopendir(course.location.name)
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
grading_policy.write(dumps(course.grading_policy, cls=EdxJSONEncoder))
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index 632619e9f70e..ad3cbce9df96 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -2,6 +2,7 @@
import os
import mimetypes
from path import path
+import json
from xblock.fields import Scope
@@ -22,6 +23,11 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
# now import all static assets
static_dir = course_data_path / subpath
+ try:
+ with open(course_data_path / 'policies/assets.json') as f:
+ policy = json.load(f)
+ except (IOError, ValueError) as err:
+ policy = {}
verbose = True
@@ -46,10 +52,16 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
if fullname_with_subpath.startswith('/'):
fullname_with_subpath = fullname_with_subpath[1:]
content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath)
- mime_type = mimetypes.guess_type(filename)[0]
- content = StaticContent(content_loc, filename, mime_type, data, import_path=fullname_with_subpath)
+ policy_ele = policy.get(content_loc.url(), {})
+ displayname = policy_ele.get('displayname', filename)
+ locked = policy_ele.get('locked', False)
+ mime_type = policy_ele.get('contentType', mimetypes.guess_type(filename)[0])
+ content = StaticContent(
+ content_loc, displayname, mime_type, data,
+ import_path=fullname_with_subpath, locked=locked
+ )
# first let's save a thumbnail so we can get back a thumbnail location
(thumbnail_content, thumbnail_location) = static_content_store.generate_thumbnail(content)
From 8e7fc1537fc5df134dcd6ab538bf1ea3c3556725 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Mon, 23 Sep 2013 11:19:59 -0400
Subject: [PATCH 44/92] Test asset import/export roundtrip w/ lock setting.
---
.../contentstore/tests/test_contentstore.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 32928bb2fdcf..6f40ac0e539e 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -170,6 +170,16 @@ def check_edit_unit(self, test_course_name):
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
self.assertEqual(resp.status_code, 200)
+ def lockAnAsset(self, content_store, course_location):
+ """
+ Lock an arbitrary asset in the course
+ :param course_location:
+ """
+ course_assets = content_store.get_all_content_for_course(course_location)
+ self.assertGreater(len(course_assets), 0, "No assets to lock")
+ content_store.set_attr(course_assets[0]['_id'], 'locked', True)
+ return course_assets[0]['_id']
+
def test_edit_unit_toy(self):
self.check_edit_unit('toy')
@@ -952,6 +962,11 @@ def test_export_course(self, mock_get):
self.assertIn(private_location_no_draft.url(), sequential.children)
+ locked_asset = self.lockAnAsset(content_store, location)
+ locked_asset_attrs = content_store.get_attrs(locked_asset)
+ # the later import will reupload
+ del locked_asset_attrs['uploadDate']
+
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
@@ -1034,6 +1049,10 @@ def test_export_course(self, mock_get):
self.assertGreater(len(course.textbooks), 0)
+ new_attrs = content_store.get_attrs(locked_asset)
+ for key, value in locked_asset_attrs.iteritems():
+ self.assertEqual(value, new_attrs[key])
+
shutil.rmtree(root_dir)
def test_export_course_with_metadata_only_video(self):
From 20e4d585c85361d6742cc40a9a84ec9afbaf5683 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Tue, 24 Sep 2013 16:30:24 -0400
Subject: [PATCH 45/92] Add documentation and comments.
---
common/lib/xmodule/xmodule/contentstore/mongo.py | 13 +++++++++++--
.../lib/xmodule/xmodule/modulestore/xml_importer.py | 2 ++
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py
index 0322b394ca73..dcc58b343185 100644
--- a/common/lib/xmodule/xmodule/contentstore/mongo.py
+++ b/common/lib/xmodule/xmodule/contentstore/mongo.py
@@ -104,7 +104,16 @@ def export(self, location, output_directory):
with disk_fs.open(content.name, 'wb') as asset_file:
asset_file.write(content.data)
- def export_all_for_course(self, course_location, output_directory, policy_file):
+ def export_all_for_course(self, course_location, output_directory, assets_policy_file):
+ """
+ Export all of this course's assets to the output_directory. Export all of the assets'
+ attributes to the policy file.
+
+ :param course_location: the Location of type 'course'
+ :param output_directory: the directory under which to put all the asset files
+ :param assets_policy_file: the filename for the policy file which should be in the same
+ directory as the other policy files.
+ """
policy = {}
assets = self.get_all_content_for_course(course_location)
@@ -115,7 +124,7 @@ def export_all_for_course(self, course_location, output_directory, policy_file):
if attr not in ['_id', 'md5', 'uploadDate', 'length', 'chunkSize']:
policy.setdefault(asset_location.url(), {})[attr] = value
- with open(policy_file, 'w') as f:
+ with open(assets_policy_file, 'w') as f:
json.dump(policy, f)
def get_all_content_thumbnails_for_course(self, location):
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index ad3cbce9df96..64caad048122 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -27,6 +27,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
with open(course_data_path / 'policies/assets.json') as f:
policy = json.load(f)
except (IOError, ValueError) as err:
+ # xml backed courses won't have this file, only exported courses; so, its absence is not
+ # really an exception.
policy = {}
verbose = True
From 4ee9ef61cf43f10c5d7c81f93974c02dad122262 Mon Sep 17 00:00:00 2001
From: Diana Huang
Date: Tue, 24 Sep 2013 14:11:00 -0400
Subject: [PATCH 46/92] Clean up some old pep8/pylint violations
Also, deletes some unused code.
---
common/djangoapps/course_modes/views.py | 27 +++++--
lms/djangoapps/verify_student/models.py | 32 ++++----
lms/djangoapps/verify_student/ssencrypt.py | 35 +++++++--
lms/djangoapps/verify_student/urls.py | 7 --
lms/djangoapps/verify_student/views.py | 89 +++++-----------------
5 files changed, 86 insertions(+), 104 deletions(-)
diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index cf4dadd1a0c6..02934f365096 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -1,12 +1,15 @@
+"""
+Views for the course_mode module
+"""
+
import decimal
from django.core.urlresolvers import reverse
from django.http import (
- HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404
+ HttpResponseBadRequest, Http404
)
from django.shortcuts import redirect
from django.views.generic.base import View
from django.utils.translation import ugettext as _
-from django.utils.http import urlencode
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
@@ -18,10 +21,19 @@
from student.views import course_from_id
from verify_student.models import SoftwareSecurePhotoVerification
-class ChooseModeView(View):
+class ChooseModeView(View):
+ """
+ View used when the user is asked to pick a mode
+
+ When a get request is used, shows the selection page.
+ When a post request is used, assumes that it is a form submission
+ from the selection page, parses the response, and then sends user
+ to the next step in the flow
+ """
@method_decorator(login_required)
def get(self, request, course_id, error=None):
+ """ Displays the course mode choice page """
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard'))
modes = CourseMode.modes_for_course_dict(course_id)
@@ -34,8 +46,8 @@ def get(self, request, course_id, error=None):
"course_id": course_id,
"modes": modes,
"course_name": course.display_name_with_default,
- "course_org" : course.display_org_with_default,
- "course_num" : course.display_number_with_default,
+ "course_org": course.display_org_with_default,
+ "course_num": course.display_number_with_default,
"chosen_price": chosen_price,
"error": error,
}
@@ -48,6 +60,7 @@ def get(self, request, course_id, error=None):
@method_decorator(login_required)
def post(self, request, course_id):
+ """ Takes the form submission from the page and parses it """
user = request.user
# This is a bit redundant with logic in student.views.change_enrollement,
@@ -102,6 +115,10 @@ def post(self, request, course_id):
)
def get_requested_mode(self, user_choice):
+ """
+ Given the text of `user_choice`, return the
+ corresponding course mode slug
+ """
choices = {
"Select Audit": "audit",
"Select Certificate": "verified"
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index 5a1821de5417..80e05d09b13a 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -26,12 +26,11 @@
from django.core.urlresolvers import reverse
from django.db import models
from django.contrib.auth.models import User
-from django.core.urlresolvers import reverse
from model_utils.models import StatusModel
from model_utils import Choices
from verify_student.ssencrypt import (
- random_aes_key, decode_and_decrypt, encrypt_and_encode,
+ random_aes_key, encrypt_and_encode,
generate_signed_message, rsa_encrypt
)
@@ -57,15 +56,18 @@ def refund_user(self, user_id):
distracting boilerplate when looking at a Model that needs to go through a
workflow process.
"""
- def decorator_func(fn):
- @functools.wraps(fn)
+ def decorator_func(func):
+ """
+ Decorator function that gets returned
+ """
+ @functools.wraps(func)
def with_status_check(obj, *args, **kwargs):
if obj.status not in valid_start_statuses:
exception_msg = (
u"Error calling {} {}: status is '{}', must be one of: {}"
- ).format(fn, obj, obj.status, valid_start_statuses)
+ ).format(func, obj, obj.status, valid_start_statuses)
raise VerificationException(exception_msg)
- return fn(obj, *args, **kwargs)
+ return func(obj, *args, **kwargs)
return with_status_check
@@ -367,7 +369,7 @@ def system_error(self,
Status should be moved to `must_retry`.
"""
if self.status in ["approved", "denied"]:
- return # If we were already approved or denied, just leave it.
+ return # If we were already approved or denied, just leave it.
self.error_msg = error_msg
self.error_code = error_code
@@ -408,7 +410,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
# encode that. The result is saved here. Actual expected length is 344.
photo_id_key = models.TextField(max_length=1024)
- IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
+ IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
@status_before_must_be("created")
def upload_face_image(self, img_data):
@@ -444,8 +446,8 @@ def submit(self):
self.status = "must_retry"
self.error_msg = response.text
self.save()
- except Exception as e:
- log.exception(e)
+ except Exception as error:
+ log.exception(error)
def image_url(self, name):
"""
@@ -466,7 +468,7 @@ def _generate_key(self, prefix):
bucket = conn.get_bucket(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["S3_BUCKET"])
key = Key(bucket)
- key.key = "{}/{}".format(prefix, self.receipt_id);
+ key.key = "{}/{}".format(prefix, self.receipt_id)
return key
@@ -507,7 +509,7 @@ def create_request(self):
"Content-Type": "application/json",
"Date": formatdate(timeval=None, localtime=False, usegmt=True)
}
- message, _, authorization = generate_signed_message(
+ _message, _sig, authorization = generate_signed_message(
"POST", headers, body, access_key, secret_key
)
headers['Authorization'] = authorization
@@ -515,16 +517,18 @@ def create_request(self):
return headers, body
def request_message_txt(self):
+ """ This is the body of the request we send across """
headers, body = self.create_request()
header_txt = "\n".join(
- "{}: {}".format(h, v) for h,v in sorted(headers.items())
+ "{}: {}".format(h, v) for h, v in sorted(headers.items())
)
body_txt = json.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8')
return header_txt + "\n\n" + body_txt
def send_request(self):
+ """ sends the request across to the endpoint """
headers, body = self.create_request()
response = requests.post(
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"],
@@ -538,4 +542,4 @@ def send_request(self):
log.debug("Return code: {}".format(response.status_code))
log.debug("Return message:\n\n{}\n\n".format(response.text))
- return response
\ No newline at end of file
+ return response
diff --git a/lms/djangoapps/verify_student/ssencrypt.py b/lms/djangoapps/verify_student/ssencrypt.py
index aefb4292a0c0..bbf32d7a8be3 100644
--- a/lms/djangoapps/verify_student/ssencrypt.py
+++ b/lms/djangoapps/verify_student/ssencrypt.py
@@ -22,16 +22,11 @@
according to a certain pass phrase. Only OpenSSL-compatible pass phrases are
supported.
"""
-from collections import OrderedDict
-from email.utils import formatdate
from hashlib import md5, sha256
-from uuid import uuid4
import base64
import binascii
-import json
import hmac
import logging
-import sys
from Crypto import Random
from Crypto.Cipher import AES, PKCS1_OAEP
@@ -39,12 +34,17 @@
log = logging.getLogger(__name__)
+
def encrypt_and_encode(data, key):
+ """ Encrypts and endcodes `data` using `key' """
return base64.urlsafe_b64encode(aes_encrypt(data, key))
+
def decode_and_decrypt(encoded_data, key):
+ """ Decrypts and decodes `data` using `key' """
return aes_decrypt(base64.urlsafe_b64decode(encoded_data), key)
+
def aes_encrypt(data, key):
"""
Return a version of the `data` that has been encrypted to
@@ -53,11 +53,16 @@ def aes_encrypt(data, key):
padded_data = pad(data)
return cipher.encrypt(padded_data)
+
def aes_decrypt(encrypted_data, key):
+ """
+ Decrypt `encrypted_data` using `key`
+ """
cipher = aes_cipher_from_key(key)
padded_data = cipher.decrypt(encrypted_data)
return unpad(padded_data)
+
def aes_cipher_from_key(key):
"""
Given an AES key, return a Cipher object that has `encrypt()` and
@@ -66,6 +71,7 @@ def aes_cipher_from_key(key):
"""
return AES.new(key, AES.MODE_CBC, generate_aes_iv(key))
+
def generate_aes_iv(key):
"""
Return the initialization vector Software Secure expects for a given AES
@@ -73,17 +79,23 @@ def generate_aes_iv(key):
"""
return md5(key + md5(key).hexdigest()).hexdigest()[:AES.block_size]
+
def random_aes_key():
return Random.new().read(32)
+
def pad(data):
+ """ Pad the given `data` such that it fits into the proper AES block size """
bytes_to_pad = AES.block_size - len(data) % AES.block_size
return data + (bytes_to_pad * chr(bytes_to_pad))
+
def unpad(padded_data):
+ """ remove all padding from `padded_data` """
num_padded_bytes = ord(padded_data[-1])
return padded_data[:-num_padded_bytes]
+
def rsa_encrypt(data, rsa_pub_key_str):
"""
`rsa_pub_key` is a string with the public key
@@ -93,11 +105,16 @@ def rsa_encrypt(data, rsa_pub_key_str):
encrypted_data = cipher.encrypt(data)
return encrypted_data
+
def rsa_decrypt(data, rsa_priv_key_str):
+ """
+ When given some `data` and an RSA private key, decrypt the data
+ """
key = RSA.importKey(rsa_priv_key_str)
cipher = PKCS1_OAEP.new(key)
return cipher.decrypt(data)
+
def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key):
"""
Given a message (either request or response), say whether it has a valid
@@ -123,6 +140,7 @@ def has_valid_signature(method, headers_dict, body_dict, access_key, secret_key)
return True
+
def generate_signed_message(method, headers_dict, body_dict, access_key, secret_key):
"""
Returns a (message, signature) pair.
@@ -137,6 +155,7 @@ def generate_signed_message(method, headers_dict, body_dict, access_key, secret_
message += '\n'
return message, signature, authorization_header
+
def signing_format_message(method, headers_dict, body_dict):
"""
Given a dictionary of headers and a dictionary of the JSON for the body,
@@ -149,6 +168,7 @@ def signing_format_message(method, headers_dict, body_dict):
return message
+
def header_string(headers_dict):
"""Given a dictionary of headers, return a canonical string representation."""
header_list = []
@@ -160,7 +180,8 @@ def header_string(headers_dict):
if 'Content-MD5' in headers_dict:
header_list.append(headers_dict['Content-MD5'] + "\n")
- return "".join(header_list) # Note that trailing \n's are important
+ return "".join(header_list) # Note that trailing \n's are important
+
def body_string(body_dict, prefix=""):
"""
@@ -183,5 +204,5 @@ def body_string(body_dict, prefix=""):
value = "null"
body_list.append(u"{}{}:{}\n".format(prefix, key, value).encode('utf-8'))
- return "".join(body_list) # Note that trailing \n's are important
+ return "".join(body_list) # Note that trailing \n's are important
diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py
index 52c55ad452d4..15c2cc5f6b29 100644
--- a/lms/djangoapps/verify_student/urls.py
+++ b/lms/djangoapps/verify_student/urls.py
@@ -35,11 +35,4 @@
name="verify_student_results_callback",
),
- url(
- r'^show_verification_page/(?P[^/]+/[^/]+/[^/]+)$',
- views.show_verification_page,
- name="verify_student/show_verification_page"
- ),
-
-
)
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 329e7efa102f..b220ff6a97d1 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -1,5 +1,5 @@
"""
-
+Views for the verification flow
"""
import json
@@ -37,6 +37,12 @@ class VerifyView(View):
@method_decorator(login_required)
def get(self, request, course_id):
"""
+ Displays the main verification view, which contains three separate steps:
+ - Taking the standard face photo
+ - Taking the id photo
+ - Confirming that the photos and payment price are correct
+ before proceeding to payment
+
"""
# If the user has already been verified within the given time period,
# redirect straight to the payment -- no need to verify again.
@@ -69,8 +75,8 @@ def get(self, request, course_id):
"user_full_name": request.user.profile.name,
"course_id": course_id,
"course_name": course.display_name_with_default,
- "course_org" : course.display_org_with_default,
- "course_num" : course.display_number_with_default,
+ "course_org": course.display_org_with_default,
+ "course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"suggested_prices": [
decimal.Decimal(price)
@@ -106,8 +112,8 @@ def get(self, request, course_id):
context = {
"course_id": course_id,
"course_name": course.display_name_with_default,
- "course_org" : course.display_org_with_default,
- "course_num" : course.display_number_with_default,
+ "course_org": course.display_org_with_default,
+ "course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"currency": verify_mode.currency.upper(),
"chosen_price": chosen_price,
@@ -162,8 +168,9 @@ def create_order(request):
return HttpResponse(json.dumps(params), content_type="text/json")
+
@require_POST
-@csrf_exempt # SS does its own message signing, and their API won't have a cookie value
+@csrf_exempt # SS does its own message signing, and their API won't have a cookie value
def results_callback(request):
"""
Software Secure will call this callback to tell us whether a user is
@@ -194,7 +201,7 @@ def results_callback(request):
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_SECRET_KEY"]
)
- _, access_key_and_sig = headers["Authorization"].split(" ")
+ _response, access_key_and_sig = headers["Authorization"].split(" ")
access_key = access_key_and_sig.split(":")[0]
# This is what we should be doing...
@@ -234,10 +241,11 @@ def results_callback(request):
return HttpResponse("OK!")
+
@login_required
def show_requirements(request, course_id):
"""
- Show the requirements necessary for
+ Show the requirements necessary for the verification flow.
"""
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified':
return redirect(reverse('dashboard'))
@@ -246,69 +254,8 @@ def show_requirements(request, course_id):
context = {
"course_id": course_id,
"course_name": course.display_name_with_default,
- "course_org" : course.display_org_with_default,
- "course_num" : course.display_number_with_default,
+ "course_org": course.display_org_with_default,
+ "course_num": course.display_number_with_default,
"is_not_active": not request.user.is_active,
}
return render_to_response("verify_student/show_requirements.html", context)
-
-
-def show_verification_page(request):
- pass
-
-def enroll(user, course_id, mode_slug):
- """
- Enroll the user in a course for a certain mode.
-
- This is the view you send folks to when they click on the enroll button.
- This does NOT cover changing enrollment modes -- it's intended for new
- enrollments only, and will just redirect to the dashboard if it detects
- that an enrollment already exists.
- """
- # If the user is already enrolled, jump to the dashboard. Yeah, we could
- # do upgrades here, but this method is complicated enough.
- if CourseEnrollment.is_enrolled(user, course_id):
- return HttpResponseRedirect(reverse('dashboard'))
-
- available_modes = CourseModes.modes_for_course(course_id)
-
- # If they haven't chosen a mode...
- if not mode_slug:
- # Does this course support multiple modes of Enrollment? If so, redirect
- # to a page that lets them choose which mode they want.
- if len(available_modes) > 1:
- return HttpResponseRedirect(
- reverse('choose_enroll_mode', kwargs={'course_id': course_id})
- )
- # Otherwise, we use the only mode that's supported...
- else:
- mode_slug = available_modes[0].slug
-
- # If the mode is one of the simple, non-payment ones, do the enrollment and
- # send them to their dashboard.
- if mode_slug in ("honor", "audit"):
- CourseEnrollment.enroll(user, course_id, mode=mode_slug)
- return HttpResponseRedirect(reverse('dashboard'))
-
- if mode_slug == "verify":
- if SoftwareSecurePhotoVerification.has_submitted_recent_request(user):
- # Capture payment info
- # Create an order
- # Create a VerifiedCertificate order item
- return HttpResponse.Redirect(reverse('verified'))
-
- # There's always at least one mode available (default is "honor"). If they
- # haven't specified a mode, we just assume it's
- if not mode:
- mode = available_modes[0]
-
- elif len(available_modes) == 1:
- if mode != available_modes[0]:
- raise Exception()
-
- mode = available_modes[0]
-
- if mode == "honor":
- CourseEnrollment.enroll(user, course_id)
- return HttpResponseRedirect(reverse('dashboard'))
-
From f1ccf1c06b12acd4c74db693363c1fc6abd1f2a0 Mon Sep 17 00:00:00 2001
From: Renzo Lucioni
Date: Mon, 9 Sep 2013 14:52:54 -0400
Subject: [PATCH 47/92] Integrate split testing and LMS tabs experiments
---
lms/djangoapps/courseware/tabs.py | 99 ++++++++++++-------
lms/djangoapps/courseware/tests/test_tabs.py | 57 +++++------
lms/djangoapps/courseware/views.py | 1 +
lms/envs/common.py | 16 ++-
lms/envs/dev.py | 2 +-
.../course/layout/_courseware_header.scss | 11 +++
.../courseware/course_navigation.html | 11 ++-
lms/templates/courseware/welcome-back.html | 34 ++++++-
lms/templates/dashboard.html | 8 +-
lms/templates/widgets/segment-io.html | 16 ++-
lms/urls.py | 1 +
requirements/edx/github.txt | 1 +
12 files changed, 184 insertions(+), 73 deletions(-)
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index ce49e5a2013e..6579e631d6de 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -25,6 +25,8 @@
from open_ended_grading import open_ended_notifications
+import waffle
+
log = logging.getLogger(__name__)
@@ -55,32 +57,46 @@ def CourseTab(name, link, is_active, has_img=False, img=""):
##### Generators for various tabs.
-
-def _courseware(tab, user, course, active_page):
+def _courseware(tab, user, course, active_page, request):
+ """
+ This returns a tab containing the course content.
+ """
link = reverse('courseware', args=[course.id])
- return [CourseTab('Courseware', link, active_page == "courseware")]
+ if waffle.flag_is_active(request, 'merge_course_tabs'):
+ return [CourseTab('Course Content', link, active_page == "courseware")]
+ else:
+ return [CourseTab('Courseware', link, active_page == "courseware")]
-def _course_info(tab, user, course, active_page):
+def _course_info(tab, user, course, active_page, request):
+ """
+ This returns a tab containing information about the course.
+ """
link = reverse('info', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "info")]
-def _progress(tab, user, course, active_page):
+def _progress(tab, user, course, active_page, request):
+ """
+ This returns a tab containing information about the authenticated user's progress.
+ """
if user.is_authenticated():
link = reverse('progress', args=[course.id])
return [CourseTab(tab['name'], link, active_page == "progress")]
return []
-def _wiki(tab, user, course, active_page):
+def _wiki(tab, user, course, active_page, request):
+ """
+ This returns a tab containing the course wiki.
+ """
if settings.WIKI_ENABLED:
link = reverse('course_wiki', args=[course.id])
return [CourseTab(tab['name'], link, active_page == 'wiki')]
return []
-def _discussion(tab, user, course, active_page):
+def _discussion(tab, user, course, active_page, request):
"""
This tab format only supports the new Berkeley discussion forums.
"""
@@ -91,25 +107,25 @@ def _discussion(tab, user, course, active_page):
return []
-def _external_discussion(tab, user, course, active_page):
+def _external_discussion(tab, user, course, active_page, request):
"""
This returns a tab that links to an external discussion service
"""
return [CourseTab('Discussion', tab['link'], active_page == 'discussion')]
-def _external_link(tab, user, course, active_page):
+def _external_link(tab, user, course, active_page, request):
# external links are never active
return [CourseTab(tab['name'], tab['link'], False)]
-def _static_tab(tab, user, course, active_page):
+def _static_tab(tab, user, course, active_page, request):
link = reverse('static_tab', args=[course.id, tab['url_slug']])
active_str = 'static_tab_{0}'.format(tab['url_slug'])
return [CourseTab(tab['name'], link, active_page == active_str)]
-def _textbooks(tab, user, course, active_page):
+def _textbooks(tab, user, course, active_page, request):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
@@ -120,7 +136,8 @@ def _textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.textbooks)]
return []
-def _pdf_textbooks(tab, user, course, active_page):
+
+def _pdf_textbooks(tab, user, course, active_page, request):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
@@ -131,7 +148,8 @@ def _pdf_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.pdf_textbooks)]
return []
-def _html_textbooks(tab, user, course, active_page):
+
+def _html_textbooks(tab, user, course, active_page, request):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
@@ -142,7 +160,8 @@ def _html_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.html_textbooks)]
return []
-def _staff_grading(tab, user, course, active_page):
+
+def _staff_grading(tab, user, course, active_page, request):
if has_access(user, course, 'staff'):
link = reverse('staff_grading', args=[course.id])
@@ -157,14 +176,13 @@ def _staff_grading(tab, user, course, active_page):
return []
-def _syllabus(tab, user, course, active_page):
+def _syllabus(tab, user, course, active_page, request):
"""Display the syllabus tab"""
link = reverse('syllabus', args=[course.id])
return [CourseTab('Syllabus', link, active_page == 'syllabus')]
-def _peer_grading(tab, user, course, active_page):
-
+def _peer_grading(tab, user, course, active_page, request):
if user.is_authenticated():
link = reverse('peer_grading', args=[course.id])
tab_name = "Peer grading"
@@ -178,7 +196,7 @@ def _peer_grading(tab, user, course, active_page):
return []
-def _combined_open_ended_grading(tab, user, course, active_page):
+def _combined_open_ended_grading(tab, user, course, active_page, request):
if user.is_authenticated():
link = reverse('open_ended_notifications', args=[course.id])
tab_name = "Open Ended Panel"
@@ -191,15 +209,15 @@ def _combined_open_ended_grading(tab, user, course, active_page):
return tab
return []
-def _notes_tab(tab, user, course, active_page):
+
+def _notes_tab(tab, user, course, active_page, request):
if user.is_authenticated() and settings.MITX_FEATURES.get('ENABLE_STUDENT_NOTES'):
link = reverse('notes', args=[course.id])
return [CourseTab(tab['name'], link, active_page == 'notes')]
return []
-#### Validators
-
+#### Validators
def key_checker(expected_keys):
"""
Returns a function that checks that specified keys are present in a dict
@@ -263,12 +281,15 @@ def validate_tabs(course):
if len(tabs) < 2:
raise InvalidTabsException("Expected at least two tabs. tabs: '{0}'".format(tabs))
+
if tabs[0]['type'] != 'courseware':
raise InvalidTabsException(
"Expected first tab to have type 'courseware'. tabs: '{0}'".format(tabs))
+
if tabs[1]['type'] != 'course_info':
raise InvalidTabsException(
"Expected second tab to have type 'course_info'. tabs: '{0}'".format(tabs))
+
for t in tabs:
if t['type'] not in VALID_TAB_TYPES:
raise InvalidTabsException("Unknown tab type {0}. Known types: {1}"
@@ -280,12 +301,12 @@ def validate_tabs(course):
# are actually unique (otherwise, will break active tag code)
-def get_course_tabs(user, course, active_page):
+def get_course_tabs(user, course, active_page, request):
"""
Return the tabs to show a particular user, as a list of CourseTab items.
"""
if not hasattr(course, 'tabs') or not course.tabs:
- return get_default_tabs(user, course, active_page)
+ return get_default_tabs(user, course, active_page, request)
# TODO (vshnayder): There needs to be a place to call this right after course
# load, but not from inside xmodule, since that doesn't (and probably
@@ -293,12 +314,18 @@ def get_course_tabs(user, course, active_page):
validate_tabs(course)
tabs = []
- for tab in course.tabs:
+
+ if waffle.flag_is_active(request, 'merge_course_tabs'):
+ course_tabs = [tab for tab in course.tabs if tab['type'] != "course_info"]
+ else:
+ course_tabs = course.tabs
+
+ for tab in course_tabs:
# expect handlers to return lists--handles things that are turned off
# via feature flags, and things like 'textbook' which might generate
# multiple tabs.
gen = VALID_TAB_TYPES[tab['type']].generator
- tabs.extend(gen(tab, user, course, active_page))
+ tabs.extend(gen(tab, user, course, active_page, request))
# Instructor tab is special--automatically added if user is staff for the course
if has_access(user, course, 'staff'):
@@ -314,7 +341,7 @@ def get_discussion_link(course):
Return the URL for the discussion tab for the given `course`.
If they have a discussion link specified, use that even if we disable
- discussions. Disabling discsussions is mostly a server safety feature at
+ discussions. Disabling discussions is mostly a server safety feature at
this point, and we don't need to worry about external sites. Otherwise,
if the course has a discussion tab or uses the default tabs, return the
discussion view URL. Otherwise, return None to indicate the lack of a
@@ -330,28 +357,33 @@ def get_discussion_link(course):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])
-def get_default_tabs(user, course, active_page):
-
+def get_default_tabs(user, course, active_page, request):
+ """
+ Return the default set of tabs.
+ """
# When calling the various _tab methods, can omit the 'type':'blah' from the
# first arg, since that's only used for dispatch
tabs = []
- tabs.extend(_courseware({''}, user, course, active_page))
- tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page))
+
+ tabs.extend(_courseware({''}, user, course, active_page, request))
+
+ if not waffle.flag_is_active(request, 'merge_course_tabs'):
+ tabs.extend(_course_info({'name': 'Course Info'}, user, course, active_page, request))
if hasattr(course, 'syllabus_present') and course.syllabus_present:
link = reverse('syllabus', args=[course.id])
tabs.append(CourseTab('Syllabus', link, active_page == 'syllabus'))
- tabs.extend(_textbooks({}, user, course, active_page))
+ tabs.extend(_textbooks({}, user, course, active_page, request))
discussion_link = get_discussion_link(course)
if discussion_link:
tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
- tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
+ tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page, request))
if user.is_authenticated() and not course.hide_progress_tab:
- tabs.extend(_progress({'name': 'Progress'}, user, course, active_page))
+ tabs.extend(_progress({'name': 'Progress'}, user, course, active_page, request))
if has_access(user, course, 'staff'):
link = reverse('instructor_dashboard', args=[course.id])
@@ -376,7 +408,6 @@ def get_static_tab_by_slug(course, tab_slug):
def get_static_tab_contents(request, course, tab):
-
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(course.id,
request.user, modulestore().get_instance(course.id, loc), depth=0)
diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py
index 5de7a39f63d0..54264121d155 100644
--- a/lms/djangoapps/courseware/tests/test_tabs.py
+++ b/lms/djangoapps/courseware/tests/test_tabs.py
@@ -11,6 +11,7 @@
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
+FAKE_REQUEST = None
class ProgressTestCase(TestCase):
@@ -29,20 +30,20 @@ def setUp(self):
def test_progress(self):
self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course,
- self.active_page0), [])
+ self.active_page0, FAKE_REQUEST), [])
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
- self.active_page1)[0].name, 'same')
+ self.active_page1, FAKE_REQUEST)[0].name, 'same')
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
- self.active_page1)[0].link,
+ self.active_page1, FAKE_REQUEST)[0].link,
reverse('progress', args=[self.course.id]))
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
- self.active_page0)[0].is_active, False)
+ self.active_page0, FAKE_REQUEST)[0].is_active, False)
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
- self.active_page1)[0].is_active, True)
+ self.active_page1, FAKE_REQUEST)[0].is_active, True)
class WikiTestCase(TestCase):
@@ -60,26 +61,26 @@ def setUp(self):
def test_wiki_enabled(self):
self.assertEqual(tabs._wiki(self.tab, self.user,
- self.course, self.active_page1)[0].name,
+ self.course, self.active_page1, FAKE_REQUEST)[0].name,
'same')
self.assertEqual(tabs._wiki(self.tab, self.user,
- self.course, self.active_page1)[0].link,
+ self.course, self.active_page1, FAKE_REQUEST)[0].link,
reverse('course_wiki', args=[self.course.id]))
self.assertEqual(tabs._wiki(self.tab, self.user,
- self.course, self.active_page1)[0].is_active,
+ self.course, self.active_page1, FAKE_REQUEST)[0].is_active,
True)
self.assertEqual(tabs._wiki(self.tab, self.user,
- self.course, self.active_page0)[0].is_active,
+ self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False)
@override_settings(WIKI_ENABLED=False)
def test_wiki_enabled_false(self):
self.assertEqual(tabs._wiki(self.tab, self.user,
- self.course, self.active_page1), [])
+ self.course, self.active_page1, FAKE_REQUEST), [])
class ExternalLinkTestCase(TestCase):
@@ -95,19 +96,19 @@ def setUp(self):
def test_external_link(self):
self.assertEqual(tabs._external_link(self.tabby, self.user,
- self.course, self.active_page0)[0].name,
+ self.course, self.active_page0, FAKE_REQUEST)[0].name,
'same')
self.assertEqual(tabs._external_link(self.tabby, self.user,
- self.course, self.active_page0)[0].link,
+ self.course, self.active_page0, FAKE_REQUEST)[0].link,
'blink')
self.assertEqual(tabs._external_link(self.tabby, self.user,
- self.course, self.active_page0)[0].is_active,
+ self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False)
self.assertEqual(tabs._external_link(self.tabby, self.user,
- self.course, self.active_page00)[0].is_active,
+ self.course, self.active_page00, FAKE_REQUEST)[0].is_active,
False)
@@ -125,20 +126,20 @@ def setUp(self):
def test_static_tab(self):
self.assertEqual(tabs._static_tab(self.tabby, self.user,
- self.course, self.active_page1)[0].name,
+ self.course, self.active_page1, FAKE_REQUEST)[0].name,
'same')
self.assertEqual(tabs._static_tab(self.tabby, self.user,
- self.course, self.active_page1)[0].link,
+ self.course, self.active_page1, FAKE_REQUEST)[0].link,
reverse('static_tab', args=[self.course.id,
self.tabby['url_slug']]))
self.assertEqual(tabs._static_tab(self.tabby, self.user,
- self.course, self.active_page1)[0].is_active,
+ self.course, self.active_page1, FAKE_REQUEST)[0].is_active,
True)
self.assertEqual(tabs._static_tab(self.tabby, self.user,
- self.course, self.active_page0)[0].is_active,
+ self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
False)
@@ -166,45 +167,45 @@ def setUp(self):
def test_textbooks1(self):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page0)[0].name,
+ self.course, self.active_page0, FAKE_REQUEST)[0].name,
'Algebra')
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page0)[0].link,
+ self.course, self.active_page0, FAKE_REQUEST)[0].link,
reverse('book', args=[self.course.id, 0]))
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page0)[0].is_active,
+ self.course, self.active_page0, FAKE_REQUEST)[0].is_active,
True)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_pageX)[0].is_active,
+ self.course, self.active_pageX, FAKE_REQUEST)[0].is_active,
False)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page1)[1].name,
+ self.course, self.active_page1, FAKE_REQUEST)[1].name,
'Topology')
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page1)[1].link,
+ self.course, self.active_page1, FAKE_REQUEST)[1].link,
reverse('book', args=[self.course.id, 1]))
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_page1)[1].is_active,
+ self.course, self.active_page1, FAKE_REQUEST)[1].is_active,
True)
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_pageX)[1].is_active,
+ self.course, self.active_pageX, FAKE_REQUEST)[1].is_active,
False)
@override_settings(MITX_FEATURES={'ENABLE_TEXTBOOK': False})
def test_textbooks0(self):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
- self.course, self.active_pageX), [])
+ self.course, self.active_pageX, FAKE_REQUEST), [])
self.assertEqual(tabs._textbooks(self.tab, self.mockuser0,
- self.course, self.active_pageX), [])
+ self.course, self.active_pageX, FAKE_REQUEST), [])
class KeyCheckerTestCase(TestCase):
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 695d7ea55dd2..66c91f1b9bea 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -728,6 +728,7 @@ def submission_history(request, course_id, student_username, location):
Right now this only works for problems because that's all
StudentModuleHistory records.
"""
+
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 466ec262f909..47f0083957ae 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -80,7 +80,7 @@
'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard)
- 'ENABLE_DJANGO_ADMIN_SITE': False, # set true to enable django's admin site, even on prod (e.g. for course ops)
+ 'ENABLE_DJANGO_ADMIN_SITE': True, # set true to enable django's admin site, even on prod (e.g. for course ops)
'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False,
'ENABLE_MANUAL_GIT_RELOAD': False,
@@ -523,6 +523,14 @@
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
+################################# Waffle ###################################
+
+# Name prepended to cookies set by Waffle
+WAFFLE_COOKIE = "waffle_flag_%s"
+
+# Two weeks (in sec)
+WAFFLE_MAX_AGE = 1209600
+
################################# Middleware ###################################
# List of finder classes that know how to find static files in
# various locations.
@@ -570,6 +578,9 @@
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
'ratelimitbackend.middleware.RateLimitMiddleware',
+
+ # For A/B testing
+ 'waffle.middleware.WaffleMiddleware',
)
############################### Pipeline #######################################
@@ -832,6 +843,9 @@
# Foldit integration
'foldit',
+ # For A/B testing
+ 'waffle',
+
# For testing
'django.contrib.admin', # only used in DEBUG mode
'django_nose',
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index 1ec5030f7ad4..c596208b3f5b 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -255,7 +255,7 @@
##### segment-io ######
-# If there's an environment variable set, grab it and turn on segment io
+# If there's an environment variable set, grab it and turn on Segment.io
SEGMENT_IO_LMS_KEY = os.environ.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = True
diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss
index 77a80b481c46..95765fc93c19 100644
--- a/lms/static/sass/course/layout/_courseware_header.scss
+++ b/lms/static/sass/course/layout/_courseware_header.scss
@@ -23,6 +23,17 @@ nav.course-material {
list-style: none;
margin-right: 6px;
+ &.prominent {
+ margin-right: 16px;
+ background: rgba(255, 255, 255, .5);
+ border-radius: 3px;
+ }
+
+ &.prominent + li {
+ padding-left: 15px;
+ border-left: 1px solid #333;
+ }
+
a {
border-radius: 3px;
color: #555;
diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html
index 303a12f14238..8cd5368ad05f 100644
--- a/lms/templates/courseware/course_navigation.html
+++ b/lms/templates/courseware/course_navigation.html
@@ -13,19 +13,24 @@
%>
<%! from courseware.tabs import get_course_tabs %>
<%! from django.utils.translation import ugettext as _ %>
+<% import waffle %>