diff --git a/README.md b/README.md
index 4ab07b3550..0261f87b46 100644
--- a/README.md
+++ b/README.md
@@ -17,10 +17,10 @@ installation process.
Providers. You should use VirtualBox >= 4.2.12.
(Windows: later/earlier VirtualBox versions than 4.2.12 have been reported to not work well with
Vagrant. If this is still a problem, you can
- install 4.2.12 from https://www.virtualbox.org/wiki/Download_Old_Builds_4_2).
+ install 4.2.12 from http://download.virtualbox.org/virtualbox/4.2.12/).
4. Install Vagrant: http://www.vagrantup.com/ (Vagrant 1.2.2 or later)
5. Open a terminal
-6. Download the project: `git clone git://github.com/edx/edx-platform.git`
+6. Download the project: `git clone https://github.com/edx/edx-platform.git`
7. Enter the project directory: `cd edx-platform/`
8. (Windows only) Run the commands to
[deal with line endings and symlinks under Windows](https://github.com/edx/edx-platform/wiki/Simplified-install-with-vagrant#dealing-with-line-endings-and-symlinks-under-windows)
diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py
index 90d8b3c668..37d647fd1a 100644
--- a/cms/djangoapps/contentstore/management/commands/populate_creators.py
+++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py
@@ -39,7 +39,7 @@ class Command(BaseCommand):
# added with status granted above, and add_user_with_status_unrequested
# will not try to add them again if they already exist in the course creator database.
for user in get_users_with_staff_role():
- add_user_with_status_unrequested(admin, user)
+ add_user_with_status_unrequested(user)
# There could be users who are not in either staff or instructor (they've
# never actually done anything in Studio). I plan to add those as unrequested
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index a51110163d..b15c05b984 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -1359,3 +1359,63 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(course.textbooks, fetched_course.textbooks)
# is this test too strict? i.e., it requires the dicts to be ==
self.assertEqual(course.checklists, fetched_course.checklists)
+
+
+class MetadataSaveTestCase(ModuleStoreTestCase):
+ """
+ Test that metadata is correctly decached.
+ """
+
+ def setUp(self):
+ sample_xml = '''
+
+ '''
+ CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
+ course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
+
+ model_data = {'data': sample_xml}
+ self.descriptor = ItemFactory.create(parent_location=course_location, category='video', data=model_data)
+
+ def test_metadata_persistence(self):
+ """
+ Test that descriptors which set metadata fields in their
+ constructor are correctly persisted.
+ """
+ # We should start with a source field, from the XML's tag
+ self.assertIn('source', own_metadata(self.descriptor))
+ attrs_to_strip = {
+ 'show_captions',
+ 'youtube_id_1_0',
+ 'youtube_id_0_75',
+ 'youtube_id_1_25',
+ 'youtube_id_1_5',
+ 'start_time',
+ 'end_time',
+ 'source',
+ 'track'
+ }
+ # We strip out all metadata fields to reproduce a bug where
+ # constructors which set their fields (e.g. Video) didn't have
+ # those changes persisted. So in the end we have the XML data
+ # in `descriptor.data`, but not in the individual fields
+ fields = self.descriptor.fields
+ for field in fields:
+ if field.name in attrs_to_strip:
+ field.delete_from(self.descriptor)
+
+ # Assert that we correctly stripped the field
+ self.assertNotIn('source', own_metadata(self.descriptor))
+ get_modulestore(self.descriptor.location).update_metadata(
+ self.descriptor.location,
+ own_metadata(self.descriptor)
+ )
+ module = get_modulestore(self.descriptor.location).get_item(self.descriptor.location)
+ # Assert that get_item correctly sets the metadata
+ self.assertIn('source', own_metadata(module))
diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
index d4df2e1054..39dc6afe0e 100644
--- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
+++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
@@ -53,6 +53,13 @@ CONTAINER_XMODULES = (
CourseDescriptor,
)
+# These modules are editable in studio yet
+NOT_STUDIO_EDITABLE = (
+ CrowdsourceHinterDescriptor,
+ GraphicalSliderToolDescriptor,
+ PollDescriptor
+)
+
class TestXBlockWrapper(object):
@@ -60,12 +67,11 @@ class TestXBlockWrapper(object):
def leaf_module_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs))
- runtime.anonymous_student_id = 'anonymous_student_id'
+ runtime.anonymous_student_id = 'dummy_anonymous_student_id'
runtime.open_ended_grading_interface = {}
runtime.seed = 5
runtime.get = lambda x: getattr(runtime, x)
- runtime.position = 2
- runtime.ajax_url = 'ajax_url'
+ runtime.ajax_url = 'dummy_ajax_url'
runtime.xblock_model_data = lambda d: d._model_data
return runtime
@@ -78,7 +84,7 @@ class TestXBlockWrapper(object):
def leaf_descriptor(self, descriptor_cls):
return descriptor_cls(
self.leaf_descriptor_runtime,
- {'location': 'i4x://org/course/catagory/name'}
+ {'location': 'i4x://org/course/category/name'}
)
def leaf_module(self, descriptor_cls):
@@ -90,6 +96,7 @@ class TestXBlockWrapper(object):
runtime.get_module.side_effect = lambda x: self.leaf_module(HtmlDescriptor)
else:
runtime.get_module.side_effect = lambda x: self.container_module(VerticalDescriptor, depth-1)
+ runtime.position = 2
return runtime
@property
@@ -102,7 +109,7 @@ class TestXBlockWrapper(object):
return descriptor_cls(
self.container_descriptor_runtime,
{
- 'location': 'i4x://org/course/catagory/name',
+ 'location': 'i4x://org/course/category/name',
'children': range(3)
}
)
@@ -158,3 +165,62 @@ class TestStudentView(TestXBlockWrapper):
def check_student_view_container_node_xblocks_only(self, descriptor_cls):
raise SkipTest("XBlock support in XModules not yet fully implemented")
+
+class TestStudioView(TestXBlockWrapper):
+
+ # Test that for all of the Descriptors listed in LEAF_XMODULES,
+ # the studio_view wrapper returns the same thing in its content
+ # as get_html returns
+ def test_studio_view_leaf_node(self):
+ for descriptor_cls in LEAF_XMODULES:
+ yield self.check_studio_view_leaf_node, descriptor_cls
+
+ # Check that when a descriptor is instantiated from descriptor_cls
+ # it generates the same thing from studio_view that it does from get_html
+ def check_studio_view_leaf_node(self, descriptor_cls):
+ if descriptor_cls in NOT_STUDIO_EDITABLE:
+ raise SkipTest(descriptor_cls.__name__ + "is not editable in studio")
+
+ descriptor = self.leaf_descriptor(descriptor_cls)
+ assert_equal(descriptor.get_html(), descriptor.studio_view(None).content)
+
+
+ # Test that for all of the Descriptors listed in CONTAINER_XMODULES
+ # render the same thing using studio_view as they do using get_html, under the following conditions:
+ # a) All of its descendents are xmodules
+ # b) Some of its descendents are xmodules and some are xblocks
+ # c) All of its descendents are xblocks
+ def test_studio_view_container_node(self):
+ for descriptor_cls in CONTAINER_XMODULES:
+ yield self.check_studio_view_container_node_xmodules_only, descriptor_cls
+ yield self.check_studio_view_container_node_mixed, descriptor_cls
+ yield self.check_studio_view_container_node_xblocks_only, descriptor_cls
+
+
+ # Check that when a descriptor is generated from descriptor_cls
+ # with only xmodule children, it generates the same html from studio_view
+ # as it does using get_html
+ def check_studio_view_container_node_xmodules_only(self, descriptor_cls):
+ if descriptor_cls in NOT_STUDIO_EDITABLE:
+ raise SkipTest(descriptor_cls.__name__ + "is not editable in studio")
+
+ descriptor = self.container_descriptor(descriptor_cls)
+ assert_equal(descriptor.get_html(), descriptor.studio_view(None).content)
+
+ # Check that when a descriptor is generated from descriptor_cls
+ # with mixed xmodule and xblock children, it generates the same html from studio_view
+ # as it does using get_html
+ def check_studio_view_container_node_mixed(self, descriptor_cls):
+ if descriptor_cls in NOT_STUDIO_EDITABLE:
+ raise SkipTest(descriptor_cls.__name__ + "is not editable in studio")
+
+ raise SkipTest("XBlock support in XDescriptor not yet fully implemented")
+
+ # Check that when a descriptor is generated from descriptor_cls
+ # with only xblock children, it generates the same html from studio_view
+ # as it does using get_html
+ def check_studio_view_container_node_xblocks_only(self, descriptor_cls):
+ if descriptor_cls in NOT_STUDIO_EDITABLE:
+ raise SkipTest(descriptor_cls.__name__ + "is not editable in studio")
+
+ raise SkipTest("XBlock support in XModules not yet fully implemented")
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index f53a2b5f8b..65c9a0e981 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -794,6 +794,18 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
return metadata_fields
+ # ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
+ def studio_view(self, context):
+ """
+ Return a fragment with the html from this XModuleDescriptor's editing view
+
+ Doesn't yet add any of the javascript to the fragment, nor the css.
+ Also doesn't expect any javascript binding, yet.
+
+ Makes no use of the context parameter
+ """
+ return Fragment(self.get_html())
+
class DescriptorSystem(object):
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
diff --git a/lms/djangoapps/user_api/serializers.py b/lms/djangoapps/user_api/serializers.py
index 42c42cf891..8822817933 100644
--- a/lms/djangoapps/user_api/serializers.py
+++ b/lms/djangoapps/user_api/serializers.py
@@ -14,8 +14,8 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
# This list is the minimal set required by the notification service
- fields = ("id", "email", "name")
- read_only_fields = ("id", "email")
+ fields = ("id", "email", "name", "username")
+ read_only_fields = ("id", "email", "username")
class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
diff --git a/lms/djangoapps/user_api/tests/test_views.py b/lms/djangoapps/user_api/tests/test_views.py
index 1e8e6f8074..075c1f0d9f 100644
--- a/lms/djangoapps/user_api/tests/test_views.py
+++ b/lms/djangoapps/user_api/tests/test_views.py
@@ -74,7 +74,7 @@ class UserApiTestCase(TestCase):
def assertUserIsValid(self, user):
"""Assert that the given user result is valid"""
- self.assertItemsEqual(user.keys(), ["email", "id", "name", "url"])
+ self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "url"])
self.assertSelfReferential(user)
def assertPrefIsValid(self, pref):
@@ -196,7 +196,8 @@ class UserViewSetTest(UserApiTestCase):
"email": user.email,
"id": user.id,
"name": user.profile.name,
- "url": uri,
+ "username": user.username,
+ "url": uri
}
)
@@ -334,6 +335,7 @@ class UserPreferenceViewSetTest(UserApiTestCase):
"email": pref.user.email,
"id": pref.user.id,
"name": pref.user.profile.name,
+ "username": pref.user.username,
"url": self.get_uri_for_user(pref.user),
},
"key": pref.key,
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index 79e3af89a1..97bc325d35 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -1,11 +1,11 @@
# Python libraries to install directly from github
# Third-party:
--e git://github.com/edx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
--e git://github.com/edx/django-pipeline.git#egg=django-pipeline
--e git://github.com/edx/django-wiki.git@41815e2ef1b0323f92900f8e60711b0f0c37766b#egg=django-wiki
--e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
--e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
+-e git+https://github.com/edx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
+-e git+https://github.com/edx/django-pipeline.git#egg=django-pipeline
+-e git+https://github.com/edx/django-wiki.git@41815e2ef1b0323f92900f8e60711b0f0c37766b#egg=django-wiki
+-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
+-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries:
-e git+https://github.com/edx/XBlock.git@b697bebd45deebd0f868613fab6722a0460ca0c1#egg=XBlock
diff --git a/scripts/release-email-list.sh b/scripts/release-email-list.sh
index f54018f4f5..246a449273 100755
--- a/scripts/release-email-list.sh
+++ b/scripts/release-email-list.sh
@@ -1,9 +1,20 @@
#! /bin/bash
+# Usage: release-email-list.sh [$PREVIOUS_COMMIT [$CURRENT_COMMIT]]
+#
+# Prints a list of email addresses and a Confluence style wiki table
+# that indicate all of the changes made between $PREVIOUS_COMMIT and $CURRENT_COMMIT
+#
+# PREVIOUS_COMMIT defaults to origin/release
+# CURRENT_COMMIT defaults to HEAD
-LOG_CMD="git --no-pager log $1..$2"
+BASE=${1:-origin/release}
+CURRENT=${2:-HEAD}
+LOG_CMD="git --no-pager log $BASE..$CURRENT"
RESPONSIBLE=$(sort -u <($LOG_CMD --format='tformat:%ae' && $LOG_CMD --format='tformat:%ce'))
+echo "Comparing $BASE to $CURRENT"
+
echo "~~~~ Email ~~~~~"
echo -n 'To: '