diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 51a98f2de7..b7d1d7a5ce 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,9 @@ Studio: Send e-mails to new Studio users (on edge only) when their course creato
status has changed. This will not be in use until the course creator table
is enabled.
+Studio: Added improvements to Course Creation: richer error messaging, tip
+text, and fourth field for course run.
+
LMS: Added user preferences (arbitrary user/key/value tuples, for which
which user/key is unique) and a REST API for reading users and
preferences. Access to the REST API is restricted by use of the
@@ -18,6 +21,9 @@ the setting is not present, the API is disabled).
LMS: Added endpoints for AJAX requests to enable/disable notifications
(which are not yet implemented) and a one-click unsubscribe page.
+Studio: Allow instructors of a course to designate other staff as instructors;
+this allows instructors to hand off management of a course to someone else.
+
Common: Add a manage.py that knows about edx-platform specific settings and projects
Common: Added *experimental* support for jsinput type.
diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py
index 0f2e60dd6e..4923851445 100644
--- a/cms/djangoapps/auth/authz.py
+++ b/cms/djangoapps/auth/authz.py
@@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name):
user.save()
-def is_user_in_course_group_role(user, location, role):
+def is_user_in_course_group_role(user, location, role, check_staff=True):
if user.is_active and user.is_authenticated:
# all "is_staff" flagged accounts belong to all groups
- return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
+ if check_staff and user.is_staff:
+ return True
+ return user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0
return False
diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 7e1e6470ff..7ea09333ed 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -1,7 +1,7 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-from lxml import html
+from lxml import html, etree
import re
from django.http import HttpResponseBadRequest
import logging
@@ -74,34 +74,44 @@ def update_course_updates(location, update, passed_id=None):
escaped = django.utils.html.escape(course_updates.data)
course_html_parsed = html.fromstring("
" + escaped + "
")
+ # if there's no ol, create it
+ if course_html_parsed.tag != 'ol':
+ # surround whatever's there w/ an ol
+ if course_html_parsed.tag != 'li':
+ # but first wrap in an li
+ li = etree.Element('li')
+ li.append(course_html_parsed)
+ course_html_parsed = li
+ ol = etree.Element('ol')
+ ol.append(course_html_parsed)
+ course_html_parsed = ol
+
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('
' + update['date'] + '
' + update['content'] + '
')
- # Confirm that root is , iterate over
, pull out
subs and then rest of val
- if course_html_parsed.tag == 'ol':
- # ??? Should this use the id in the json or in the url or does it matter?
- if passed_id is not None:
- idx = get_idx(passed_id)
- # idx is count from end of list
- course_html_parsed[-idx] = new_html_parsed
- else:
- course_html_parsed.insert(0, new_html_parsed)
+ # ??? Should this use the id in the json or in the url or does it matter?
+ if passed_id is not None:
+ idx = get_idx(passed_id)
+ # idx is count from end of list
+ course_html_parsed[-idx] = new_html_parsed
+ else:
+ course_html_parsed.insert(0, new_html_parsed)
- idx = len(course_html_parsed)
- passed_id = course_updates.location.url() + "/" + str(idx)
+ idx = len(course_html_parsed)
+ passed_id = course_updates.location.url() + "/" + str(idx)
- # update db record
- course_updates.data = html.tostring(course_html_parsed)
- modulestore('direct').update_item(location, course_updates.data)
+ # update db record
+ course_updates.data = html.tostring(course_html_parsed)
+ modulestore('direct').update_item(location, course_updates.data)
- if (len(new_html_parsed) == 1):
- content = new_html_parsed[0].tail
- else:
- content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
+ if (len(new_html_parsed) == 1):
+ content = new_html_parsed[0].tail
+ else:
+ content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
- return {"id": passed_id,
- "date": update['date'],
- "content": content}
+ return {"id": passed_id,
+ "date": update['date'],
+ "content": content}
def delete_course_update(location, update, passed_id):
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index d357c8ae96..43b2b64257 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -53,6 +53,14 @@ def i_have_opened_a_new_course(_step):
open_new_course()
+@step('(I select|s?he selects) the new course')
+def select_new_course(_step, whom):
+ course_link_xpath = '//div[contains(@class, "courses")]//a[contains(@class, "class-link")]//span[contains(., "{name}")]/..'.format(
+ name="Robot Super Course")
+ element = world.browser.find_by_xpath(course_link_xpath)
+ element.click()
+
+
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(_step, name):
css = 'a.action-%s' % name.lower()
@@ -118,14 +126,18 @@ def create_studio_user(
registration.register(studio_user)
registration.activate()
+ return studio_user
+
def fill_in_course_info(
name='Robot Super Course',
org='MITx',
- num='999'):
+ num='101',
+ run='2013_Spring'):
world.css_fill('.new-course-name', name)
world.css_fill('.new-course-org', org)
world.css_fill('.new-course-number', num)
+ world.css_fill('.new-course-run', run)
def log_into_studio(
@@ -242,7 +254,7 @@ def save_button_disabled(step):
@step('I confirm the prompt')
def confirm_the_prompt(step):
prompt_css = 'a.button.action-primary'
- world.css_click(prompt_css)
+ world.css_click(prompt_css, success_condition=lambda: not world.css_visible(prompt_css))
@step(u'I am shown a (.*)$')
@@ -252,6 +264,7 @@ def i_am_shown_a_notification(step, notification_type):
def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index)
+ world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')")
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
if world.is_mac():
g._element.send_keys(Keys.COMMAND + 'a')
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index 2b206e4466..513eb699e9 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -12,11 +12,20 @@ def create_component_instance(step, component_button_css, category,
has_multiple_templates=True):
click_new_component_button(step, component_button_css)
+ if category in ('problem', 'html'):
+ def animation_done(_driver):
+ return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
+ world.wait_for(animation_done)
if has_multiple_templates:
click_component_from_menu(category, boilerplate, expected_css)
- assert_equal(1, len(world.css_find(expected_css)))
+ assert_equal(
+ 1,
+ len(world.css_find(expected_css)),
+ "Component instance with css {css} was not created successfully".format(css=expected_css))
+
+
@world.absorb
def click_new_component_button(step, component_button_css):
@@ -39,11 +48,13 @@ def click_component_from_menu(category, boilerplate, expected_css):
elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
assert_equal(len(elements), 1)
- world.css_click(elem_css)
+ world.wait_for(lambda _driver: world.css_visible(elem_css))
+ world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
+ world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
world.css_click('a.edit-button')
world.css_click('#settings-mode')
diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature
index b3041b9b18..a9aed5d982 100644
--- a/cms/djangoapps/contentstore/features/course-overview.feature
+++ b/cms/djangoapps/contentstore/features/course-overview.feature
@@ -63,3 +63,10 @@ Feature: Course Overview
When I navigate to the course overview page
And I change an assignment's grading status
Then I am shown a notification
+
+ Scenario: Notification is shown on subsection reorder
+ Given I have opened a new course section in Studio
+ And I have added a new subsection
+ And I have added a new subsection
+ When I reorder subsections
+ Then I am shown a notification
diff --git a/cms/djangoapps/contentstore/features/course-overview.py b/cms/djangoapps/contentstore/features/course-overview.py
index 10fa6453b2..8baad68fdf 100644
--- a/cms/djangoapps/contentstore/features/course-overview.py
+++ b/cms/djangoapps/contentstore/features/course-overview.py
@@ -124,3 +124,14 @@ def all_sections_are_collapsed(step):
def change_grading_status(step):
world.css_find('a.menu-toggle').click()
world.css_find('.menu li').first.click()
+
+
+@step(u'I reorder subsections')
+def reorder_subsections(_step):
+ draggable_css = 'a.drag-handle'
+ ele = world.css_find(draggable_css).first
+ ele.action_chains.drag_and_drop_by_offset(
+ ele._element,
+ 30,
+ 0
+ ).perform()
diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature
index fc1212f398..ecce174ca2 100644
--- a/cms/djangoapps/contentstore/features/course-team.feature
+++ b/cms/djangoapps/contentstore/features/course-team.feature
@@ -1,7 +1,7 @@
Feature: Course Team
As a course author, I want to be able to add others to my team
- Scenario: Users can add other users
+ Scenario: Admins can add other users
Given I have opened a new course in Studio
And the user "alice" exists
And I am viewing the course team settings
@@ -9,7 +9,7 @@ Feature: Course Team
And "alice" logs in
Then she does see the course on her page
- Scenario: Added users cannot delete or add other users
+ Scenario: Added admins cannot delete or add other users
Given I have opened a new course in Studio
And the user "bob" exists
And I am viewing the course team settings
@@ -18,7 +18,7 @@ Feature: Course Team
Then he cannot delete users
And he cannot add users
- Scenario: Users can delete other users
+ Scenario: Admins can delete other users
Given I have opened a new course in Studio
And the user "carol" exists
And I am viewing the course team settings
@@ -27,8 +27,33 @@ Feature: Course Team
And "carol" logs in
Then she does not see the course on her page
- Scenario: Users cannot add users that do not exist
+ Scenario: Admins cannot add users that do not exist
Given I have opened a new course in Studio
And I am viewing the course team settings
When I add "dennis" to the course team
Then I should see "Could not find user by email address" somewhere on the page
+
+ Scenario: Admins should be able to make other people into admins
+ Given I have opened a new course in Studio
+ And the user "emily" exists
+ And I am viewing the course team settings
+ And I add "emily" to the course team
+ When I make "emily" a course team admin
+ And "emily" logs in
+ And she selects the new course
+ And she views the course team settings
+ Then "emily" should be marked as an admin
+ And she can add users
+ And she can delete users
+
+ Scenario: Admins should be able to remove other admins
+ Given I have opened a new course in Studio
+ And the user "frank" exists as a course admin
+ And I am viewing the course team settings
+ When I remove admin rights from "frank"
+ And "frank" logs in
+ And he selects the new course
+ And he views the course team settings
+ Then "frank" should not be marked as an admin
+ And he cannot add users
+ And he cannot delete users
diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py
index ad5d31977c..18456b15f7 100644
--- a/cms/djangoapps/contentstore/features/course-team.py
+++ b/cms/djangoapps/contentstore/features/course-team.py
@@ -3,65 +3,105 @@
from lettuce import world, step
from common import create_studio_user, log_into_studio
+from django.contrib.auth.models import Group
+from auth.authz import get_course_groupname_for_role
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
-@step(u'I am viewing the course team settings')
-def view_grading_settings(_step):
+@step(u'(I am viewing|s?he views) the course team settings')
+def view_grading_settings(_step, whom):
world.click_course_settings()
link_css = 'li.nav-course-settings-team a'
world.css_click(link_css)
-@step(u'the user "([^"]*)" exists$')
-def create_other_user(_step, name):
- create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
+@step(u'the user "([^"]*)" exists( as a course admin)?$')
+def create_other_user(_step, name, course_admin):
+ user = create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
+ if course_admin:
+ location = world.scenario_dict["COURSE"].location
+ for role in ("staff", "instructor"):
+ group, __ = Group.objects.get_or_create(name=get_course_groupname_for_role(location, role))
+ user.groups.add(group)
+ user.save()
@step(u'I add "([^"]*)" to the course team')
def add_other_user(_step, name):
- new_user_css = 'a.new-user-button'
+ new_user_css = 'a.create-user-button'
world.css_click(new_user_css)
+ world.wait(0.5)
- email_css = 'input.email-input'
+ email_css = 'input#user-email-input'
f = world.css_find(email_css)
f._element.send_keys(name, EMAIL_EXTENSION)
- confirm_css = '#add_user'
+ confirm_css = 'form.create-user button.action-primary'
world.css_click(confirm_css)
@step(u'I delete "([^"]*)" from the course team')
def delete_other_user(_step, name):
- to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION)
+ to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
+ email="{0}{1}".format(name, EMAIL_EXTENSION))
world.css_click(to_delete_css)
+@step(u'I make "([^"]*)" a course team admin')
+def make_course_team_admin(_step, name):
+ admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format(
+ email=name+EMAIL_EXTENSION)
+ world.css_click(admin_btn_css)
+
+
+@step(u'I remove admin rights from "([^"]*)"')
+def remove_course_team_admin(_step, name):
+ admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
+ email=name+EMAIL_EXTENSION)
+ world.css_click(admin_btn_css)
+
+
@step(u'"([^"]*)" logs in$')
def other_user_login(_step, name):
log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION)
@step(u's?he does( not)? see the course on (his|her) page')
-def see_course(_step, doesnt_see_course, gender):
+def see_course(_step, inverted, gender):
class_css = 'span.class-name'
all_courses = world.css_find(class_css, wait_time=1)
all_names = [item.html for item in all_courses]
- if doesnt_see_course:
+ if inverted:
assert not world.scenario_dict['COURSE'].display_name in all_names
else:
assert world.scenario_dict['COURSE'].display_name in all_names
-@step(u's?he cannot delete users')
-def cannot_delete(_step):
+@step(u'"([^"]*)" should( not)? be marked as an admin')
+def marked_as_admin(_step, name, inverted):
+ flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format(
+ email=name+EMAIL_EXTENSION)
+ if inverted:
+ assert world.is_css_not_present(flag_css)
+ else:
+ assert world.is_css_present(flag_css)
+
+
+@step(u's?he can(not)? delete users')
+def can_delete_users(_step, inverted):
to_delete_css = 'a.remove-user'
- assert world.is_css_not_present(to_delete_css)
+ if inverted:
+ assert world.is_css_not_present(to_delete_css)
+ else:
+ assert world.is_css_present(to_delete_css)
-@step(u's?he cannot add users')
-def cannot_add(_step):
- add_css = 'a.new-user'
- assert world.is_css_not_present(add_css)
+@step(u's?he can(not)? add users')
+def can_add_users(_step, inverted):
+ add_css = 'a.create-user-button'
+ if inverted:
+ assert world.is_css_not_present(add_css)
+ else:
+ assert world.is_css_present(add_css)
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index 565a35f802..d7ccb557ba 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -155,6 +155,10 @@ def cancel_does_not_save_changes(step):
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
+
+ def animation_done(_driver):
+ return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none'
+ world.wait_for(animation_done)
# Go to advanced tab.
world.css_click('#ui-id-2')
world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature
index 01c912deca..c249ad61e8 100644
--- a/cms/djangoapps/contentstore/features/signup.feature
+++ b/cms/djangoapps/contentstore/features/signup.feature
@@ -8,8 +8,7 @@ Feature: Sign in
When I click the link with the text "Sign Up"
And I fill in the registration form
And I press the Create My Account button on the registration form
- Then I should see be on the studio home page
- And I should see the message "complete your sign up we need you to verify your email address"
+ Then I should see an email verification prompt
Scenario: Login with a valid redirect
Given I have opened a new course in Studio
diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py
index e9abb55a78..94c6e6f18e 100644
--- a/cms/djangoapps/contentstore/features/signup.py
+++ b/cms/djangoapps/contentstore/features/signup.py
@@ -22,14 +22,10 @@ def i_press_the_button_on_the_registration_form(step):
world.css_click(submit_css)
-@step('I should see be on the studio home page$')
-def i_should_see_be_on_the_studio_home_page(step):
- step.given('I should see the message "My Courses"')
-
-
-@step(u'I should see the message "([^"]*)"$')
-def i_should_see_the_message(step, msg):
- assert world.browser.is_text_present(msg, 5)
+@step('I should see an email verification prompt')
+def i_should_see_an_email_verification_prompt(step):
+ world.css_has_text('h1.page-header', u'My Courses')
+ world.css_has_text('div.msg h3.title', u'We need to verify your email address')
@step(u'I fill in and submit the signin form$')
diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py
index 0c700956e3..df63b26b3b 100644
--- a/cms/djangoapps/contentstore/features/upload.py
+++ b/cms/djangoapps/contentstore/features/upload.py
@@ -58,7 +58,7 @@ def delete_file(_step, file_name):
world.css_click(delete_css, index=index)
prompt_confirm_css = 'li.nav-item > a.action-primary'
- world.css_click(prompt_confirm_css)
+ world.css_click(prompt_confirm_css, success_condition=lambda: not world.css_visible(prompt_confirm_css))
@step(u'I should see only one "([^"]*)"$')
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index 93d638e621..6113f42c91 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -19,5 +19,6 @@ def i_see_the_correct_settings_and_values(step):
@step('I have set "show captions" to (.*)')
def set_show_captions(step, setting):
world.css_click('a.edit-button')
+ world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.browser.select('Show Captions', setting)
world.css_click('a.save-button')
diff --git a/cms/djangoapps/contentstore/management/commands/clone.py b/cms/djangoapps/contentstore/management/commands/clone.py
index 0ca50acb50..f20625d7f2 100644
--- a/cms/djangoapps/contentstore/management/commands/clone.py
+++ b/cms/djangoapps/contentstore/management/commands/clone.py
@@ -1,6 +1,6 @@
-###
-### Script for cloning a course
-###
+"""
+Script for cloning a course
+"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore
@@ -15,23 +15,25 @@ from auth.authz import _copy_course_group
class Command(BaseCommand):
+ """Clone a MongoDB-backed course to another location"""
help = 'Clone a MongoDB backed course to another location'
def handle(self, *args, **options):
+ "Execute the command"
if len(args) != 2:
raise CommandError("clone requires two arguments: ")
source_location_str = args[0]
dest_location_str = args[1]
- ms = modulestore('direct')
- cs = contentstore()
+ mstore = modulestore('direct')
+ cstore = contentstore()
- print "Cloning course {0} to {1}".format(source_location_str, dest_location_str)
+ print("Cloning course {0} to {1}".format(source_location_str, dest_location_str))
source_location = CourseDescriptor.id_to_location(source_location_str)
dest_location = CourseDescriptor.id_to_location(dest_location_str)
- if clone_course(ms, cs, source_location, dest_location):
- print "copying User permissions..."
+ if clone_course(mstore, cstore, source_location, dest_location):
+ print("copying User permissions...")
_copy_course_group(source_location, dest_location)
diff --git a/cms/djangoapps/contentstore/management/commands/dump_course_structure.py b/cms/djangoapps/contentstore/management/commands/dump_course_structure.py
index d9b7c55cbd..139c603172 100644
--- a/cms/djangoapps/contentstore/management/commands/dump_course_structure.py
+++ b/cms/djangoapps/contentstore/management/commands/dump_course_structure.py
@@ -1,3 +1,6 @@
+"""
+Script for dumping course dumping the course structure
+"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
@@ -9,10 +12,14 @@ filter_list = ['xml_attributes', 'checklists']
class Command(BaseCommand):
+ """
+ The Django command for dumping course structure
+ """
help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized
in a JSON format. This can be used for analytics.'''
def handle(self, *args, **options):
+ "Execute the command"
if len(args) < 2 or len(args) > 3:
raise CommandError("dump_course_structure requires two or more arguments: ||")
@@ -32,7 +39,7 @@ class Command(BaseCommand):
try:
course = store.get_item(loc, depth=4)
except:
- print 'Could not find course at {0}'.format(course_id)
+ print('Could not find course at {0}'.format(course_id))
return
info = {}
diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py
index 90db8750d9..efeb5dc339 100644
--- a/cms/djangoapps/contentstore/management/commands/export.py
+++ b/cms/djangoapps/contentstore/management/commands/export.py
@@ -1,6 +1,6 @@
-###
-### Script for exporting courseware from Mongo to a tar.gz file
-###
+"""
+Script for exporting courseware from Mongo to a tar.gz file
+"""
import os
from django.core.management.base import BaseCommand, CommandError
@@ -10,20 +10,21 @@ from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
-unnamed_modules = 0
-
-
class Command(BaseCommand):
+ """
+ Export the specified data directory into the default ModuleStore
+ """
help = 'Export the specified data directory into the default ModuleStore'
def handle(self, *args, **options):
+ "Execute the command"
if len(args) != 2:
raise CommandError("export requires two arguments: