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: '