Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into bug/dhm/dec12
This commit is contained in:
@@ -55,6 +55,22 @@ def create_new_course_group(creator, location, role):
|
||||
|
||||
return
|
||||
|
||||
'''
|
||||
This is to be called only by either a command line code path or through a app which has already
|
||||
asserted permissions
|
||||
'''
|
||||
def _delete_course_group(location):
|
||||
# remove all memberships
|
||||
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
|
||||
for user in instructors.user_set.all():
|
||||
user.groups.remove(instructors)
|
||||
user.save()
|
||||
|
||||
staff = Group.objects.get(name=get_course_groupname_for_role(location, STAFF_ROLE_NAME))
|
||||
for user in staff.user_set.all():
|
||||
user.groups.remove(staff)
|
||||
user.save()
|
||||
|
||||
|
||||
def add_user_to_course_group(caller, user, location, role):
|
||||
# only admins can add/remove other users
|
||||
|
||||
31
cms/djangoapps/contentstore/management/commands/clone.py
Normal file
31
cms/djangoapps/contentstore/management/commands/clone.py
Normal file
@@ -0,0 +1,31 @@
|
||||
###
|
||||
### 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
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
#
|
||||
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
|
||||
#
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Clone a MongoDB backed course to another location'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
raise CommandError("clone requires two arguments: <source-location> <dest-location>")
|
||||
|
||||
source_location_str = args[0]
|
||||
dest_location_str = args[1]
|
||||
|
||||
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)
|
||||
|
||||
clone_course(modulestore('direct'), contentstore(), source_location, dest_location)
|
||||
@@ -0,0 +1,36 @@
|
||||
###
|
||||
### Script for cloning a course
|
||||
###
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from prompt import query_yes_no
|
||||
|
||||
from auth.authz import _delete_course_group
|
||||
|
||||
#
|
||||
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
|
||||
#
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Delete a MongoDB backed course'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1:
|
||||
raise CommandError("delete_course requires one arguments: <location>")
|
||||
|
||||
loc_str = args[0]
|
||||
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
if delete_course(modulestore('direct'), contentstore(), loc) == True:
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
_delete_course_group(loc)
|
||||
|
||||
|
||||
|
||||
33
cms/djangoapps/contentstore/management/commands/prompt.py
Normal file
33
cms/djangoapps/contentstore/management/commands/prompt.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
|
||||
def query_yes_no(question, default="yes"):
|
||||
"""Ask a yes/no question via raw_input() and return their answer.
|
||||
|
||||
"question" is a string that is presented to the user.
|
||||
"default" is the presumed answer if the user just hits <Enter>.
|
||||
It must be "yes" (the default), "no" or None (meaning
|
||||
an answer is required of the user).
|
||||
|
||||
The "answer" return value is one of "yes" or "no".
|
||||
"""
|
||||
valid = {"yes":True, "y":True, "ye":True,
|
||||
"no":False, "n":False}
|
||||
if default == None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
elif default == "no":
|
||||
prompt = " [y/N] "
|
||||
else:
|
||||
raise ValueError("invalid default answer: '%s'" % default)
|
||||
|
||||
while True:
|
||||
sys.stdout.write(question + prompt)
|
||||
choice = raw_input().lower()
|
||||
if default is not None and choice == '':
|
||||
return valid[default]
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
||||
"(or 'y' or 'n').\n")
|
||||
18
cms/djangoapps/contentstore/tests/test_utils.py
Normal file
18
cms/djangoapps/contentstore/tests/test_utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.test.testcases import TestCase
|
||||
from cms.djangoapps.contentstore import utils
|
||||
import mock
|
||||
|
||||
class LMSLinksTestCase(TestCase):
|
||||
def about_page_test(self):
|
||||
location = 'i4x','mitX','101','course', 'test'
|
||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||
link = utils.get_lms_link_for_about_page(location)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about")
|
||||
|
||||
def ls_link_test(self):
|
||||
location = 'i4x','mitX','101','vertical', 'contacting_us'
|
||||
utils.get_course_id = mock.Mock(return_value="mitX/101/test")
|
||||
link = utils.get_lms_link_for_item(location, False)
|
||||
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
||||
link = utils.get_lms_link_for_item(location, True)
|
||||
self.assertEquals(link, "//preview.localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us")
|
||||
@@ -73,20 +73,38 @@ def get_course_for_item(location):
|
||||
|
||||
|
||||
def get_lms_link_for_item(location, preview=False):
|
||||
location = Location(location)
|
||||
if settings.LMS_BASE is not None:
|
||||
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
preview='preview.' if preview else '',
|
||||
lms_base=settings.LMS_BASE,
|
||||
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
|
||||
course_id=modulestore().get_containing_courses(location)[0].id,
|
||||
location=location,
|
||||
course_id=get_course_id(location),
|
||||
location=Location(location)
|
||||
)
|
||||
else:
|
||||
lms_link = None
|
||||
|
||||
return lms_link
|
||||
|
||||
def get_lms_link_for_about_page(location):
|
||||
"""
|
||||
Returns the url to the course about page from the location tuple.
|
||||
"""
|
||||
if settings.LMS_BASE is not None:
|
||||
lms_link = "//{lms_base}/courses/{course_id}/about".format(
|
||||
lms_base=settings.LMS_BASE,
|
||||
course_id=get_course_id(location)
|
||||
)
|
||||
else:
|
||||
lms_link = None
|
||||
|
||||
return lms_link
|
||||
|
||||
def get_course_id(location):
|
||||
"""
|
||||
Returns the course_id from a given the location tuple.
|
||||
"""
|
||||
# TODO: These will need to be changed to point to the particular instance of this problem in the particular course
|
||||
return modulestore().get_containing_courses(Location(location))[0].id
|
||||
|
||||
class UnitState(object):
|
||||
draft = 'draft'
|
||||
|
||||
@@ -77,6 +77,8 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
LMS_BASE = "localhost:8000"
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# functioning cache -- it relies on caching to load its settings in places.
|
||||
|
||||
@@ -73,7 +73,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course URL</a>, and cannot be changed</span>
|
||||
<span class="tip tip-stacked">This is used in <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course URL</a>, and cannot be changed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +213,7 @@ from contentstore import utils
|
||||
<div class="field">
|
||||
<div class="input">
|
||||
<textarea class="long tall tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_item(context_course.location, True)}">your course summary page</a></span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(context_course.location)}">your course summary page</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,8 +29,7 @@ class MongoContentStore(ContentStore):
|
||||
id = content.get_id()
|
||||
|
||||
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
|
||||
if self.fs.exists({"_id" : id}):
|
||||
self.fs.delete(id)
|
||||
self.delete(id)
|
||||
|
||||
with self.fs.new_file(_id = id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
displayname=content.name, thumbnail_location=content.thumbnail_location) as fp:
|
||||
@@ -39,7 +38,10 @@ class MongoContentStore(ContentStore):
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def delete(self, id):
|
||||
if self.fs.exists({"_id" : id}):
|
||||
self.fs.delete(id)
|
||||
|
||||
def find(self, location):
|
||||
id = StaticContent.get_id_from_location(location)
|
||||
try:
|
||||
|
||||
129
common/lib/xmodule/xmodule/modulestore/store_utilities.py
Normal file
129
common/lib/xmodule/xmodule/modulestore/store_utilities.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import logging
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
|
||||
# check to see if the dest_location exists as an empty course
|
||||
# we need an empty course because the app layers manage the permissions and users
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
|
||||
if len(dest_modules) != 1:
|
||||
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
|
||||
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
|
||||
# Get all modules under this namespace which is (tag, org, course) tuple
|
||||
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
original_loc = Location(module.location)
|
||||
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course, name=dest_location.name)
|
||||
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
|
||||
if 'data' in module.definition:
|
||||
modulestore.update_item(module.location, module.definition['data'])
|
||||
|
||||
# repoint children
|
||||
if 'children' in module.definition:
|
||||
new_children = []
|
||||
for child_loc_url in module.definition['children']:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
new_children = new_children + [child_loc.url()]
|
||||
|
||||
modulestore.update_children(module.location, new_children)
|
||||
|
||||
# save metadata
|
||||
modulestore.update_metadata(module.location, module.metadata)
|
||||
|
||||
# now iterate through all of the assets and clone them
|
||||
# first the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
|
||||
contentstore.save(content)
|
||||
|
||||
# now iterate through all of the assets, also updating the thumbnail pointer
|
||||
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location._replace(tag = dest_location.tag, org = dest_location.org,
|
||||
course = dest_location.course)
|
||||
|
||||
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
|
||||
contentstore.save(content)
|
||||
|
||||
def delete_course(modulestore, contentstore, source_location):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
|
||||
# first delete all of the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
id = StaticContent.get_id_from_location(thumb_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
contentstore.delete(id)
|
||||
|
||||
# then delete all of the assets
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
contentstore.delete(id)
|
||||
|
||||
# then delete all course modules
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
print "Deleting {0}...".format(module.location)
|
||||
modulestore.delete_item(module.location)
|
||||
|
||||
# finally delete the top-level course module itself
|
||||
print "Deleting {0}...".format(source_location)
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
return True
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Formula Repsonse
|
||||
display_name: Formula Response
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
data: |
|
||||
|
||||
22
rakefile
22
rakefile
@@ -396,6 +396,28 @@ task :publish => :package do
|
||||
sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}")
|
||||
end
|
||||
|
||||
namespace :cms do
|
||||
desc "Clone existing MongoDB based course"
|
||||
task :clone do
|
||||
if ENV['SOURCE_LOC'] and ENV['DEST_LOC']
|
||||
sh(django_admin(:cms, :dev, :clone, ENV['SOURCE_LOC'], ENV['DEST_LOC']))
|
||||
else
|
||||
raise "You must pass in a SOURCE_LOC and DEST_LOC parameters"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :cms do
|
||||
desc "Delete existing MongoDB based course"
|
||||
task :delete_course do
|
||||
if ENV['LOC']
|
||||
sh(django_admin(:cms, :dev, :delete_course, ENV['LOC']))
|
||||
else
|
||||
raise "You must pass in a LOC parameter"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :cms do
|
||||
desc "Import course data within the given DATA_DIR variable"
|
||||
task :import do
|
||||
|
||||
Reference in New Issue
Block a user