Merge pull request #9809 from edx/mushtaq/force-publish-command
'force_publish' command for publishing a course forcefully by making …
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Script for force publishing a course
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from .prompt import query_yes_no
|
||||
from .utils import get_course_versions
|
||||
|
||||
# To run from command line: ./manage.py cms force_publish course-v1:org+course+run
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Force publish a course"""
|
||||
help = '''
|
||||
Force publish a course. Takes two arguments:
|
||||
<course_id>: the course id of the course you want to publish forcefully
|
||||
commit: do the force publish
|
||||
|
||||
If you do not specify 'commit', the command will print out what changes would be made.
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Execute the command"""
|
||||
if len(args) not in {1, 2}:
|
||||
raise CommandError("force_publish requires 1 or more argument: <course_id> |commit|")
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
except InvalidKeyError:
|
||||
raise CommandError("Invalid course key.")
|
||||
|
||||
if not modulestore().get_course(course_key):
|
||||
raise CommandError("Course not found.")
|
||||
|
||||
commit = False
|
||||
if len(args) == 2:
|
||||
commit = args[1] == 'commit'
|
||||
|
||||
# for now only support on split mongo
|
||||
owning_store = modulestore()._get_modulestore_for_courselike(course_key) # pylint: disable=protected-access
|
||||
if hasattr(owning_store, 'force_publish_course'):
|
||||
versions = get_course_versions(args[0])
|
||||
print "Course versions : {0}".format(versions)
|
||||
|
||||
if commit:
|
||||
if query_yes_no("Are you sure to publish the {0} course forcefully?".format(course_key), default="no"):
|
||||
# publish course forcefully
|
||||
updated_versions = owning_store.force_publish_course(
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, commit
|
||||
)
|
||||
if updated_versions:
|
||||
# if publish and draft were different
|
||||
if versions['published-branch'] != versions['draft-branch']:
|
||||
print "Success! Published the course '{0}' forcefully.".format(course_key)
|
||||
print "Updated course versions : \n{0}".format(updated_versions)
|
||||
else:
|
||||
print "Course '{0}' is already in published state.".format(course_key)
|
||||
else:
|
||||
print "Error! Could not publish course {0}.".format(course_key)
|
||||
else:
|
||||
# if publish and draft were different
|
||||
if versions['published-branch'] != versions['draft-branch']:
|
||||
print "Dry run. Following would have been changed : "
|
||||
print "Published branch version {0} changed to draft branch version {1}".format(
|
||||
versions['published-branch'], versions['draft-branch']
|
||||
)
|
||||
else:
|
||||
print "Dry run. Course '{0}' is already in published state.".format(course_key)
|
||||
else:
|
||||
raise CommandError("The owning modulestore does not support this command.")
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Tests for the force_publish management command
|
||||
"""
|
||||
import mock
|
||||
from django.core.management.base import CommandError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from contentstore.management.commands.force_publish import Command
|
||||
from contentstore.management.commands.utils import get_course_versions
|
||||
|
||||
|
||||
class TestForcePublish(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the force_publish management command
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestForcePublish, cls).setUpClass()
|
||||
cls.course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
|
||||
cls.test_user_id = ModuleStoreEnum.UserID.test
|
||||
cls.command = Command()
|
||||
|
||||
def test_no_args(self):
|
||||
"""
|
||||
Test 'force_publish' command with no arguments
|
||||
"""
|
||||
errstring = "force_publish requires 1 or more argument: <course_id> |commit"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle()
|
||||
|
||||
def test_invalid_course_key(self):
|
||||
"""
|
||||
Test 'force_publish' command with invalid course key
|
||||
"""
|
||||
errstring = "Invalid course key."
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle('TestX/TS01')
|
||||
|
||||
def test_too_many_arguments(self):
|
||||
"""
|
||||
Test 'force_publish' command with more than 2 arguments
|
||||
"""
|
||||
errstring = "force_publish requires 1 or more argument: <course_id> |commit"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode(self.course.id), 'commit', 'invalid-arg')
|
||||
|
||||
def test_course_key_not_found(self):
|
||||
"""
|
||||
Test 'force_publish' command with non-existing course key
|
||||
"""
|
||||
errstring = "Course not found."
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode('course-v1:org+course+run'))
|
||||
|
||||
def test_force_publish_non_split(self):
|
||||
"""
|
||||
Test 'force_publish' command doesn't work on non split courses
|
||||
"""
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
errstring = 'The owning modulestore does not support this command.'
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode(course.id))
|
||||
|
||||
@SharedModuleStoreTestCase.modifies_courseware
|
||||
def test_force_publish(self):
|
||||
"""
|
||||
Test 'force_publish' command
|
||||
"""
|
||||
# Add some changes to course
|
||||
chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
||||
self.store.create_child(
|
||||
self.test_user_id,
|
||||
chapter.location,
|
||||
'html',
|
||||
block_id='html_component'
|
||||
)
|
||||
|
||||
# verify that course has changes.
|
||||
self.assertTrue(self.store.has_changes(self.store.get_item(self.course.location)))
|
||||
|
||||
# get draft and publish branch versions
|
||||
versions = get_course_versions(unicode(self.course.id))
|
||||
draft_version = versions['draft-branch']
|
||||
published_version = versions['published-branch']
|
||||
|
||||
# verify that draft and publish point to different versions
|
||||
self.assertNotEqual(draft_version, published_version)
|
||||
|
||||
with mock.patch('contentstore.management.commands.force_publish.query_yes_no') as patched_yes_no:
|
||||
patched_yes_no.return_value = True
|
||||
|
||||
# force publish course
|
||||
self.command.handle(unicode(self.course.id), 'commit')
|
||||
|
||||
# verify that course has no changes
|
||||
self.assertFalse(self.store.has_changes(self.store.get_item(self.course.location)))
|
||||
|
||||
# get new draft and publish branch versions
|
||||
versions = get_course_versions(unicode(self.course.id))
|
||||
new_draft_version = versions['draft-branch']
|
||||
new_published_version = versions['published-branch']
|
||||
|
||||
# verify that the draft branch didn't change while the published branch did
|
||||
self.assertEqual(draft_version, new_draft_version)
|
||||
self.assertNotEqual(published_version, new_published_version)
|
||||
|
||||
# verify that draft and publish point to same versions now
|
||||
self.assertEqual(new_draft_version, new_published_version)
|
||||
@@ -2,6 +2,8 @@
|
||||
Common methods for cms commands to use
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
def user_from_str(identifier):
|
||||
@@ -17,3 +19,21 @@ def user_from_str(identifier):
|
||||
return User.objects.get(email=identifier)
|
||||
|
||||
return User.objects.get(id=user_id)
|
||||
|
||||
|
||||
def get_course_versions(course_key):
|
||||
"""
|
||||
Fetches the latest course versions
|
||||
:param course_key:
|
||||
:return: { 'draft-branch' : value1, 'published-branch' : value2}
|
||||
"""
|
||||
course_locator = CourseKey.from_string(course_key)
|
||||
store = modulestore()._get_modulestore_for_courselike(course_locator) # pylint: disable=protected-access
|
||||
index_entry = store.get_course_index(course_locator)
|
||||
if index_entry is not None:
|
||||
return {
|
||||
'draft-branch': index_entry['versions']['draft-branch'],
|
||||
'published-branch': index_entry['versions']['published-branch']
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@@ -445,6 +445,28 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
|
||||
if index_entry is not None:
|
||||
self._update_head(draft_course_key, index_entry, ModuleStoreEnum.BranchName.draft, new_structure['_id'])
|
||||
|
||||
def force_publish_course(self, course_locator, user_id, commit=False):
|
||||
"""
|
||||
Helper method to forcefully publish a course,
|
||||
making the published branch point to the same structure as the draft branch.
|
||||
"""
|
||||
versions = None
|
||||
index_entry = self.get_course_index(course_locator)
|
||||
if index_entry is not None:
|
||||
versions = index_entry['versions']
|
||||
if commit:
|
||||
# update published branch version only if publish and draft point to different versions
|
||||
if versions['published-branch'] != versions['draft-branch']:
|
||||
self._update_head(
|
||||
course_locator,
|
||||
index_entry,
|
||||
'published-branch',
|
||||
index_entry['versions']['draft-branch']
|
||||
)
|
||||
self._flag_publish_event(course_locator)
|
||||
return self.get_course_index(course_locator)['versions']
|
||||
return versions
|
||||
|
||||
def get_course_history_info(self, course_locator):
|
||||
"""
|
||||
See :py:meth `xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_course_history_info`
|
||||
|
||||
Reference in New Issue
Block a user