Commit 79fe2e62 by bmedx

CMS management command cleanup for Django 1.11

parent b57f1447
...@@ -4,6 +4,8 @@ erroneous certificate names. ...@@ -4,6 +4,8 @@ erroneous certificate names.
""" """
from collections import namedtuple from collections import namedtuple
from six.moves import input
from six import text_type
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
...@@ -150,10 +152,10 @@ class Command(BaseCommand): ...@@ -150,10 +152,10 @@ class Command(BaseCommand):
""" """
headers = ["Course Key", "cert_name_short", "cert_name_short", "Should clean?"] headers = ["Course Key", "cert_name_short", "cert_name_short", "Should clean?"]
col_widths = [ col_widths = [
max(len(unicode(result[col])) for result in results + [headers]) max(len(text_type(result[col])) for result in results + [headers])
for col in range(len(results[0])) for col in range(len(results[0]))
] ]
id_format = "{{:>{}}} |".format(len(unicode(len(results)))) id_format = "{{:>{}}} |".format(len(text_type(len(results))))
col_format = "| {{:>{}}} |" col_format = "| {{:>{}}} |"
self.stdout.write(id_format.format(""), ending='') self.stdout.write(id_format.format(""), ending='')
...@@ -165,7 +167,7 @@ class Command(BaseCommand): ...@@ -165,7 +167,7 @@ class Command(BaseCommand):
for idx, result in enumerate(results): for idx, result in enumerate(results):
self.stdout.write(id_format.format(idx), ending='') self.stdout.write(id_format.format(idx), ending='')
for col, width in zip(result, col_widths): for col, width in zip(result, col_widths):
self.stdout.write(col_format.format(width).format(unicode(col)), ending='') self.stdout.write(col_format.format(width).format(text_type(col)), ending='')
self.stdout.write("") self.stdout.write("")
def _commit(self, results): def _commit(self, results):
...@@ -191,7 +193,7 @@ class Command(BaseCommand): ...@@ -191,7 +193,7 @@ class Command(BaseCommand):
while True: while True:
self._display(results) self._display(results)
command = raw_input("<index>|commit|quit: ").strip() command = input("<index>|commit|quit: ").strip()
if command == 'quit': if command == 'quit':
return return
......
...@@ -24,17 +24,17 @@ class Command(BaseCommand): ...@@ -24,17 +24,17 @@ class Command(BaseCommand):
content_store = contentstore() content_store = contentstore()
success = False success = False
log.info(u"-" * 80) log.info("-" * 80)
log.info(u"Cleaning up assets for all courses") log.info("Cleaning up assets for all courses")
try: try:
# Remove all redundant Mac OS metadata files # Remove all redundant Mac OS metadata files
assets_deleted = content_store.remove_redundant_content_for_courses() assets_deleted = content_store.remove_redundant_content_for_courses()
success = True success = True
except Exception as err: except Exception as err:
log.info(u"=" * 30 + u"> failed to cleanup") log.info("=" * 30 + u"> failed to cleanup")
log.info(u"Error:") log.info("Error:")
log.info(err) log.info(err)
if success: if success:
log.info(u"=" * 80) log.info("=" * 80)
log.info(u"Total number of assets deleted: {0}".format(assets_deleted)) log.info("Total number of assets deleted: {0}".format(assets_deleted))
""" """
Script for cloning a course Script for cloning a course
""" """
from django.core.management.base import BaseCommand, CommandError from __future__ import print_function
from django.core.management.base import BaseCommand
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
...@@ -13,24 +15,30 @@ from xmodule.modulestore.django import modulestore ...@@ -13,24 +15,30 @@ from xmodule.modulestore.django import modulestore
# To run from command line: ./manage.py cms clone_course --settings=dev master/300/cough edx/111/foo # To run from command line: ./manage.py cms clone_course --settings=dev master/300/cough edx/111/foo
# #
class Command(BaseCommand): class Command(BaseCommand):
"""Clone a MongoDB-backed course to another location""" """
Clone a MongoDB-backed course to another location
"""
help = 'Clone a MongoDB backed course to another location' help = 'Clone a MongoDB backed course to another location'
def add_arguments(self, parser):
parser.add_argument('source_course_id', help='Course ID to copy from')
parser.add_argument('dest_course_id', help='Course ID to copy to')
def handle(self, *args, **options): def handle(self, *args, **options):
"Execute the command" """
if len(args) != 2: Execute the command
raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>") """
source_course_id = CourseKey.from_string(args[0]) source_course_id = CourseKey.from_string(options['source_course_id'])
dest_course_id = CourseKey.from_string(args[1]) dest_course_id = CourseKey.from_string(options['dest_course_id'])
mstore = modulestore() mstore = modulestore()
print "Cloning course {0} to {1}".format(source_course_id, dest_course_id) print("Cloning course {0} to {1}".format(source_course_id, dest_course_id))
with mstore.bulk_operations(dest_course_id): with mstore.bulk_operations(dest_course_id):
if mstore.clone_course(source_course_id, dest_course_id, ModuleStoreEnum.UserID.mgmt_command): if mstore.clone_course(source_course_id, dest_course_id, ModuleStoreEnum.UserID.mgmt_command):
print "copying User permissions..." print("copying User permissions...")
# purposely avoids auth.add_user b/c it doesn't have a caller to authorize # purposely avoids auth.add_user b/c it doesn't have a caller to authorize
CourseInstructorRole(dest_course_id).add_users( CourseInstructorRole(dest_course_id).add_users(
*CourseInstructorRole(source_course_id).users_with_role() *CourseInstructorRole(source_course_id).users_with_role()
......
""" """
Django management command to create a course in a specific modulestore Django management command to create a course in a specific modulestore
""" """
from six import text_type
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
...@@ -9,6 +11,9 @@ from contentstore.views.course import create_new_course_in_store ...@@ -9,6 +11,9 @@ from contentstore.views.course import create_new_course_in_store
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
MODULESTORE_CHOICES = (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
class Command(BaseCommand): class Command(BaseCommand):
""" """
Create a course in a specific modulestore. Create a course in a specific modulestore.
...@@ -16,45 +21,36 @@ class Command(BaseCommand): ...@@ -16,45 +21,36 @@ class Command(BaseCommand):
# can this query modulestore for the list of write accessible stores or does that violate command pattern? # can this query modulestore for the list of write accessible stores or does that violate command pattern?
help = "Create a course in one of {}".format([ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]) help = "Create a course in one of {}".format([ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split])
args = "modulestore user org course run"
def parse_args(self, *args): def add_arguments(self, parser):
parser.add_argument('modulestore',
choices=MODULESTORE_CHOICES,
help="Modulestore must be one of {}".format(MODULESTORE_CHOICES))
parser.add_argument('user',
help="The instructor's email address or integer ID.")
parser.add_argument('org',
help="The organization to create the course within.")
parser.add_argument('course',
help="The name of the course.")
parser.add_argument('run',
help="The name of the course run.")
def parse_args(self, **options):
""" """
Return a tuple of passed in values for (modulestore, user, org, course, run). Return a tuple of passed in values for (modulestore, user, org, course, run).
""" """
if len(args) != 5:
raise CommandError(
"create_course requires 5 arguments: "
"a modulestore, user, org, course, run. Modulestore is one of {}".format(
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]
)
)
if args[0] not in [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]:
raise CommandError(
"Modulestore (first arg) must be one of {}".format(
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]
)
)
storetype = args[0]
try: try:
user = user_from_str(args[1]) user = user_from_str(options['user'])
except User.DoesNotExist: except User.DoesNotExist:
raise CommandError( raise CommandError("No user {user} found.".format(user=options['user']))
"No user {user} found: expected args are {args}".format(
user=args[1],
args=self.args,
),
)
org = args[2] return options['modulestore'], user, options['org'], options['course'], options['run']
course = args[3]
run = args[4]
return storetype, user, org, course, run
def handle(self, *args, **options): def handle(self, *args, **options):
storetype, user, org, course, run = self.parse_args(*args) storetype, user, org, course, run = self.parse_args(**options)
if storetype == ModuleStoreEnum.Type.mongo:
self.stderr.write("WARNING: The 'Old Mongo' store is deprecated. New courses should be added to split.")
new_course = create_new_course_in_store(storetype, user, org, course, run, {}) new_course = create_new_course_in_store(storetype, user, org, course, run, {})
self.stdout.write(u"Created {}".format(unicode(new_course.id))) self.stdout.write(u"Created {}".format(text_type(new_course.id)))
from __future__ import print_function
from six import text_type
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -54,7 +57,7 @@ class Command(BaseCommand): ...@@ -54,7 +57,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
try: try:
# a course key may have unicode chars in it # a course key may have unicode chars in it
course_key = unicode(options['course_key'], 'utf8') course_key = text_type(options['course_key'], 'utf8')
course_key = CourseKey.from_string(course_key) course_key = CourseKey.from_string(course_key)
except InvalidKeyError: except InvalidKeyError:
raise CommandError('Invalid course_key: {}'.format(options['course_key'])) raise CommandError('Invalid course_key: {}'.format(options['course_key']))
......
"""Script for deleting orphans""" """Script for deleting orphans"""
from __future__ import print_function
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -26,15 +28,15 @@ class Command(BaseCommand): ...@@ -26,15 +28,15 @@ class Command(BaseCommand):
raise CommandError("Invalid course key.") raise CommandError("Invalid course key.")
if options['commit']: if options['commit']:
print 'Deleting orphans from the course:' print('Deleting orphans from the course:')
deleted_items = _delete_orphans( deleted_items = _delete_orphans(
course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit'] course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit']
) )
print "Success! Deleted the following orphans from the course:" print("Success! Deleted the following orphans from the course:")
print "\n".join(deleted_items) print("\n".join(deleted_items))
else: else:
print 'Dry run. The following orphans would have been deleted from the course:' print('Dry run. The following orphans would have been deleted from the course:')
deleted_items = _delete_orphans( deleted_items = _delete_orphans(
course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit'] course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit']
) )
print "\n".join(deleted_items) print("\n".join(deleted_items))
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
# Run it this way: # Run it this way:
# ./manage.py cms --settings dev edit_course_tabs --course Stanford/CS99/2013_spring # ./manage.py cms --settings dev edit_course_tabs --course Stanford/CS99/2013_spring
# #
from optparse import make_option from __future__ import print_function
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -18,10 +19,16 @@ from .prompt import query_yes_no ...@@ -18,10 +19,16 @@ from .prompt import query_yes_no
def print_course(course): def print_course(course):
"Prints out the course id and a numbered list of tabs." "Prints out the course id and a numbered list of tabs."
print course.id try:
print 'num type name' print(course.id)
for index, item in enumerate(course.tabs): print('num type name')
print index + 1, '"' + item.get('type') + '"', '"' + item.get('name', '') + '"' for index, item in enumerate(course.tabs):
print(index + 1, '"' + item.get('type') + '"', '"' + item.get('name', '') + '"')
# If a course is bad we will get an error descriptor here, dump it and die instead of
# just sending up the error that .id doesn't exist.
except AttributeError:
print(course)
raise
# course.tabs looks like this # course.tabs looks like this
...@@ -42,48 +49,50 @@ As a first step, run the command with a courseid like this: ...@@ -42,48 +49,50 @@ As a first step, run the command with a courseid like this:
This will print the existing tabs types and names. Then run the This will print the existing tabs types and names. Then run the
command again, adding --insert or --delete to edit the list. command again, adding --insert or --delete to edit the list.
""" """
# Making these option objects separately, so can refer to their .help below
course_option = make_option('--course',
action='store',
dest='course',
default=False,
help='--course <id> required, e.g. Stanford/CS99/2013_spring')
delete_option = make_option('--delete',
action='store_true',
dest='delete',
default=False,
help='--delete <tab-number>')
insert_option = make_option('--insert',
action='store_true',
dest='insert',
default=False,
help='--insert <tab-number> <type> <name>, e.g. 2 "course_info" "Course Info"')
option_list = BaseCommand.option_list + (course_option, delete_option, insert_option)
def handle(self, *args, **options): course_help = '--course <id> required, e.g. Stanford/CS99/2013_spring'
if not options['course']: delete_help = '--delete <tab-number>'
raise CommandError(Command.course_option.help) insert_help = '--insert <tab-number> <type> <name>, e.g. 4 "course_info" "Course Info"'
def add_arguments(self, parser):
parser.add_argument('--course',
dest='course',
default=False,
required=True,
help=self.course_help)
parser.add_argument('--delete',
dest='delete',
default=False,
nargs=1,
help=self.delete_help)
parser.add_argument('--insert',
dest='insert',
default=False,
nargs=3,
help=self.insert_help,
)
def handle(self, *args, **options):
course = get_course_by_id(CourseKey.from_string(options['course'])) course = get_course_by_id(CourseKey.from_string(options['course']))
print 'Warning: this command directly edits the list of course tabs in mongo.' print('Warning: this command directly edits the list of course tabs in mongo.')
print 'Tabs before any changes:' print('Tabs before any changes:')
print_course(course) print_course(course)
try: try:
if options['delete']: if options['delete']:
if len(args) != 1: num = int(options['delete'][0])
raise CommandError(Command.delete_option.help) if num < 3:
num = int(args[0]) raise CommandError("Tabs 1 and 2 cannot be changed.")
if query_yes_no('Deleting tab {0} Confirm?'.format(num), default='no'): if query_yes_no('Deleting tab {0} Confirm?'.format(num), default='no'):
tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing tabs.primitive_delete(course, num - 1) # -1 for 0-based indexing
elif options['insert']: elif options['insert']:
if len(args) != 3: num, tab_type, name = options['insert']
raise CommandError(Command.insert_option.help) num = int(num)
num = int(args[0]) if num < 3:
tab_type = args[1] raise CommandError("Tabs 1 and 2 cannot be changed.")
name = args[2]
if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default='no'): if query_yes_no('Inserting tab {0} "{1}" "{2}" Confirm?'.format(num, tab_type, name), default='no'):
tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above tabs.primitive_insert(course, num - 1, tab_type, name) # -1 as above
except ValueError as e: except ValueError as e:
......
...@@ -8,16 +8,18 @@ from .prompt import query_yes_no ...@@ -8,16 +8,18 @@ from .prompt import query_yes_no
class Command(BaseCommand): class Command(BaseCommand):
help = '''Empty the trashcan. Can pass an optional course_id to limit the damage.''' help = 'Empty the trashcan. Can pass an optional course_id to limit the damage.'
def handle(self, *args, **options): def add_arguments(self, parser):
if len(args) != 1 and len(args) != 0: parser.add_argument('course_id',
raise CommandError("empty_asset_trashcan requires one or no arguments: |<course_id>|") help='Course ID to empty, leave off to empty for all courses',
nargs='?')
if len(args) == 1: def handle(self, *args, **options):
course_ids = [CourseKey.from_string(args[0])] if options['course_id']:
course_ids = [CourseKey.from_string(options['course_id'])]
else: else:
course_ids = [course.id for course in modulestore().get_courses()] course_ids = [course.id for course in modulestore().get_courses()]
if query_yes_no("Emptying trashcan. Confirm?", default="no"): if query_yes_no("Emptying {} trashcan(s). Confirm?".format(len(course_ids)), default="no"):
empty_asset_trashcan(course_ids) empty_asset_trashcan(course_ids)
""" """
Script for exporting courseware from Mongo to a tar.gz file Script for exporting courseware from Mongo to a tar.gz file
""" """
from __future__ import print_function
import os import os
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
...@@ -37,7 +38,7 @@ class Command(BaseCommand): ...@@ -37,7 +38,7 @@ class Command(BaseCommand):
output_path = options['output_path'] output_path = options['output_path']
print "Exporting course id = {0} to {1}".format(course_key, output_path) print("Exporting course id = {0} to {1}".format(course_key, output_path))
if not output_path.endswith('/'): if not output_path.endswith('/'):
output_path += '/' output_path += '/'
......
""" """
Script for exporting all courseware from Mongo to a directory and listing the courses which failed to export Script for exporting all courseware from Mongo to a directory and listing the courses which failed to export
""" """
from django.core.management.base import BaseCommand, CommandError from __future__ import print_function
from six import text_type
from django.core.management.base import BaseCommand
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -14,23 +17,22 @@ class Command(BaseCommand): ...@@ -14,23 +17,22 @@ class Command(BaseCommand):
""" """
help = 'Export all courses from mongo to the specified data directory and list the courses which failed to export' help = 'Export all courses from mongo to the specified data directory and list the courses which failed to export'
def add_arguments(self, parser):
parser.add_argument('output_path')
def handle(self, *args, **options): def handle(self, *args, **options):
""" """
Execute the command Execute the command
""" """
if len(args) != 1: courses, failed_export_courses = export_courses_to_output_path(options['output_path'])
raise CommandError("export requires one argument: <output path>")
output_path = args[0]
courses, failed_export_courses = export_courses_to_output_path(output_path)
print "=" * 80 print("=" * 80)
print u"=" * 30 + u"> Export summary" print("=" * 30 + "> Export summary")
print u"Total number of courses to export: {0}".format(len(courses)) print("Total number of courses to export: {0}".format(len(courses)))
print u"Total number of courses which failed to export: {0}".format(len(failed_export_courses)) print("Total number of courses which failed to export: {0}".format(len(failed_export_courses)))
print u"List of export failed courses ids:" print("List of export failed courses ids:")
print u"\n".join(failed_export_courses) print("\n".join(failed_export_courses))
print "=" * 80 print("=" * 80)
def export_courses_to_output_path(output_path): def export_courses_to_output_path(output_path):
...@@ -46,15 +48,15 @@ def export_courses_to_output_path(output_path): ...@@ -46,15 +48,15 @@ def export_courses_to_output_path(output_path):
failed_export_courses = [] failed_export_courses = []
for course_id in course_ids: for course_id in course_ids:
print u"-" * 80 print("-" * 80)
print u"Exporting course id = {0} to {1}".format(course_id, output_path) print("Exporting course id = {0} to {1}".format(course_id, output_path))
try: try:
course_dir = course_id.to_deprecated_string().replace('/', '...') course_dir = course_id.to_deprecated_string().replace('/', '...')
export_course_to_xml(module_store, content_store, course_id, root_dir, course_dir) export_course_to_xml(module_store, content_store, course_id, root_dir, course_dir)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
failed_export_courses.append(unicode(course_id)) failed_export_courses.append(text_type(course_id))
print u"=" * 30 + u"> Oops, failed to export {0}".format(course_id) print("=" * 30 + "> Oops, failed to export {0}".format(course_id))
print u"Error:" print("Error:")
print err print(err)
return courses, failed_export_courses return courses, failed_export_courses
...@@ -12,7 +12,6 @@ At present, it differs from Studio exports in several ways: ...@@ -12,7 +12,6 @@ At present, it differs from Studio exports in several ways:
* The top-level directory in the resulting tarball is a "safe" * The top-level directory in the resulting tarball is a "safe"
(i.e. ascii) version of the course_key, rather than the word "course". (i.e. ascii) version of the course_key, rather than the word "course".
* It only supports the export of courses. It does not export libraries. * It only supports the export of courses. It does not export libraries.
""" """
import os import os
...@@ -34,17 +33,16 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml ...@@ -34,17 +33,16 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml
class Command(BaseCommand): class Command(BaseCommand):
""" """
Export a course to XML. The output is compressed as a tar.gz file. Export a course to XML. The output is compressed as a tar.gz file.
""" """
help = dedent(__doc__).strip() help = dedent(__doc__).strip()
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('course_id') parser.add_argument('course_id')
parser.add_argument('--output', default=None) parser.add_argument('--output')
def handle(self, *args, **options): def handle(self, *args, **options):
course_id = options['course_id'] course_id = options['course_id']
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
except InvalidKeyError: except InvalidKeyError:
...@@ -54,6 +52,7 @@ class Command(BaseCommand): ...@@ -54,6 +52,7 @@ class Command(BaseCommand):
filename = options['output'] filename = options['output']
pipe_results = False pipe_results = False
if filename is None: if filename is None:
filename = mktemp() filename = mktemp()
pipe_results = True pipe_results = True
......
...@@ -20,7 +20,7 @@ class Command(BaseCommand): ...@@ -20,7 +20,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
"""Execute the command""" """Execute the command"""
course_id = options.get('course_id', None) course_id = options['course_id']
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
# for now only support on split mongo # for now only support on split mongo
......
...@@ -44,7 +44,7 @@ class Command(BaseCommand): ...@@ -44,7 +44,7 @@ class Command(BaseCommand):
owning_store = modulestore()._get_modulestore_for_courselike(course_key) # pylint: disable=protected-access owning_store = modulestore()._get_modulestore_for_courselike(course_key) # pylint: disable=protected-access
if hasattr(owning_store, 'force_publish_course'): if hasattr(owning_store, 'force_publish_course'):
versions = get_course_versions(options['course_key']) versions = get_course_versions(options['course_key'])
print "Course versions : {0}".format(versions) print("Course versions : {0}".format(versions))
if options['commit']: if options['commit']:
if query_yes_no("Are you sure to publish the {0} course forcefully?".format(course_key), default="no"): if query_yes_no("Are you sure to publish the {0} course forcefully?".format(course_key), default="no"):
...@@ -55,20 +55,20 @@ class Command(BaseCommand): ...@@ -55,20 +55,20 @@ class Command(BaseCommand):
if updated_versions: if updated_versions:
# if publish and draft were different # if publish and draft were different
if versions['published-branch'] != versions['draft-branch']: if versions['published-branch'] != versions['draft-branch']:
print "Success! Published the course '{0}' forcefully.".format(course_key) print("Success! Published the course '{0}' forcefully.".format(course_key))
print "Updated course versions : \n{0}".format(updated_versions) print("Updated course versions : \n{0}".format(updated_versions))
else: else:
print "Course '{0}' is already in published state.".format(course_key) print("Course '{0}' is already in published state.".format(course_key))
else: else:
print "Error! Could not publish course {0}.".format(course_key) print("Error! Could not publish course {0}.".format(course_key))
else: else:
# if publish and draft were different # if publish and draft were different
if versions['published-branch'] != versions['draft-branch']: if versions['published-branch'] != versions['draft-branch']:
print "Dry run. Following would have been changed : " print("Dry run. Following would have been changed : ")
print "Published branch version {0} changed to draft branch version {1}".format( print("Published branch version {0} changed to draft branch version {1}".format(
versions['published-branch'], versions['draft-branch'] versions['published-branch'], versions['draft-branch'])
) )
else: else:
print "Dry run. Course '{0}' is already in published state.".format(course_key) print("Dry run. Course '{0}' is already in published state.".format(course_key))
else: else:
raise CommandError("The owning modulestore does not support this command.") raise CommandError("The owning modulestore does not support this command.")
...@@ -3,6 +3,7 @@ Django management command to generate a test course from a course config json ...@@ -3,6 +3,7 @@ Django management command to generate a test course from a course config json
""" """
import json import json
import logging import logging
from six import text_type
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
...@@ -57,7 +58,7 @@ class Command(BaseCommand): ...@@ -57,7 +58,7 @@ class Command(BaseCommand):
# Create the course # Create the course
try: try:
new_course = create_new_course_in_store("split", user, org, num, run, fields) new_course = create_new_course_in_store("split", user, org, num, run, fields)
logger.info("Created {}".format(unicode(new_course.id))) logger.info("Created {}".format(text_type(new_course.id)))
except DuplicateCourseError: except DuplicateCourseError:
logger.warning("Course already exists for %s, %s, %s", org, num, run) logger.warning("Course already exists for %s, %s, %s", org, num, run)
......
...@@ -14,7 +14,7 @@ attribute is set and the FEATURE['ENABLE_EXPORT_GIT'] is set. ...@@ -14,7 +14,7 @@ attribute is set and the FEATURE['ENABLE_EXPORT_GIT'] is set.
""" """
import logging import logging
from optparse import make_option from six import text_type
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -31,41 +31,34 @@ class Command(BaseCommand): ...@@ -31,41 +31,34 @@ class Command(BaseCommand):
""" """
Take a course from studio and export it to a git repository. Take a course from studio and export it to a git repository.
""" """
option_list = BaseCommand.option_list + (
make_option('--username', '-u', dest='user',
help=('Specify a username from LMS/Studio to be used '
'as the commit author.')),
make_option('--repo_dir', '-r', dest='repo',
help='Specify existing git repo directory.'),
)
help = _('Take the specified course and attempt to ' help = _('Take the specified course and attempt to '
'export it to a git repository\n. Course directory ' 'export it to a git repository\n. Course directory '
'must already be a git repository. Usage: ' 'must already be a git repository. Usage: '
' git_export <course_loc> <git_url>') ' git_export <course_loc> <git_url>')
def add_arguments(self, parser):
parser.add_argument('course_loc')
parser.add_argument('git_url')
parser.add_argument('--username', '-u', dest='user',
help='Specify a username from LMS/Studio to be used as the commit author.')
parser.add_argument('--repo_dir', '-r', dest='repo', help='Specify existing git repo directory.')
def handle(self, *args, **options): def handle(self, *args, **options):
""" """
Checks arguments and runs export function if they are good Checks arguments and runs export function if they are good
""" """
if len(args) != 2:
raise CommandError('This script requires exactly two arguments: '
'course_loc and git_url')
# Rethrow GitExportError as CommandError for SystemExit # Rethrow GitExportError as CommandError for SystemExit
try: try:
course_key = CourseKey.from_string(args[0]) course_key = CourseKey.from_string(options['course_loc'])
except InvalidKeyError: except InvalidKeyError:
raise CommandError(unicode(GitExportError.BAD_COURSE)) raise CommandError(text_type(GitExportError.BAD_COURSE))
try: try:
git_export_utils.export_to_git( git_export_utils.export_to_git(
course_key, course_key,
args[1], options['git_url'],
options.get('user', ''), options.get('user', ''),
options.get('rdir', None) options.get('rdir', None)
) )
except git_export_utils.GitExportError as ex: except git_export_utils.GitExportError as ex:
raise CommandError(unicode(ex.message)) raise CommandError(text_type(ex.message))
...@@ -3,7 +3,7 @@ Script for importing courseware from XML format ...@@ -3,7 +3,7 @@ Script for importing courseware from XML format
""" """
from optparse import make_option from optparse import make_option
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from django_comment_common.utils import are_permissions_roles_seeded, seed_permissions_roles from django_comment_common.utils import are_permissions_roles_seeded, seed_permissions_roles
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
......
...@@ -18,41 +18,34 @@ class Command(BaseCommand): ...@@ -18,41 +18,34 @@ class Command(BaseCommand):
Migrate a course from old-Mongo to split-Mongo. It reuses the old course id except where overridden. Migrate a course from old-Mongo to split-Mongo. It reuses the old course id except where overridden.
""" """
help = "Migrate a course from old-Mongo to split-Mongo. The new org, course, and run will default to the old one unless overridden" help = "Migrate a course from old-Mongo to split-Mongo. The new org, course, and run will " \
args = "course_key email <new org> <new course> <new run>" "default to the old one unless overridden."
def parse_args(self, *args): def add_arguments(self, parser):
parser.add_argument('course_key')
parser.add_argument('email')
parser.add_argument('--org', help='New org to migrate to.')
parser.add_argument('--course', help='New course key to migrate to.')
parser.add_argument('--run', help='New run to migrate to.')
def parse_args(self, **options):
""" """
Return a 5-tuple of passed in values for (course_key, user, org, course, run). Return a 5-tuple of passed in values for (course_key, user, org, course, run).
""" """
if len(args) < 2:
raise CommandError(
"migrate_to_split requires at least two arguments: "
"a course_key and a user identifier (email or ID)"
)
try: try:
course_key = CourseKey.from_string(args[0]) course_key = CourseKey.from_string(options['course_key'])
except InvalidKeyError: except InvalidKeyError:
raise CommandError("Invalid location string") raise CommandError("Invalid location string")
try: try:
user = user_from_str(args[1]) user = user_from_str(options['email'])
except User.DoesNotExist: except User.DoesNotExist:
raise CommandError("No user found identified by {}".format(args[1])) raise CommandError("No user found identified by {}".format(options['email']))
org = course = run = None
try:
org = args[2]
course = args[3]
run = args[4]
except IndexError:
pass
return course_key, user.id, org, course, run return course_key, user.id, options['org'], options['course'], options['run']
def handle(self, *args, **options): def handle(self, *args, **options):
course_key, user, org, course, run = self.parse_args(*args) course_key, user, org, course, run = self.parse_args(**options)
migrator = SplitMigrator( migrator = SplitMigrator(
source_modulestore=modulestore(), source_modulestore=modulestore(),
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
Script for granting existing course instructors course creator privileges. Script for granting existing course instructors course creator privileges.
This script is only intended to be run once on a given environment. This script is only intended to be run once on a given environment.
To run: ./manage.py cms populate_creators --settings=dev
""" """
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
...@@ -11,9 +13,6 @@ from course_creators.views import add_user_with_status_granted, add_user_with_st ...@@ -11,9 +13,6 @@ from course_creators.views import add_user_with_status_granted, add_user_with_st
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
#------------ to run: ./manage.py cms populate_creators --settings=dev
class Command(BaseCommand): class Command(BaseCommand):
""" """
Script for granting existing course instructors course creator privileges. Script for granting existing course instructors course creator privileges.
...@@ -35,23 +34,24 @@ class Command(BaseCommand): ...@@ -35,23 +34,24 @@ class Command(BaseCommand):
# the admin user will already exist. # the admin user will already exist.
admin = User.objects.get(username=username, email=email) admin = User.objects.get(username=username, email=email)
for user in get_users_with_role(CourseInstructorRole.ROLE): try:
add_user_with_status_granted(admin, user) for user in get_users_with_role(CourseInstructorRole.ROLE):
add_user_with_status_granted(admin, user)
# Some users will be both staff and instructors. Those folks have been
# 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_role(CourseStaffRole.ROLE):
add_user_with_status_unrequested(user)
# There could be users who are not in either staff or instructor (they've # Some users will be both staff and instructors. Those folks have been
# never actually done anything in Studio). I plan to add those as unrequested # added with status granted above, and add_user_with_status_unrequested
# when they first go to their dashboard. # will not try to add them again if they already exist in the course creator database.
for user in get_users_with_role(CourseStaffRole.ROLE):
add_user_with_status_unrequested(user)
admin.delete() # 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
# when they first go to their dashboard.
finally:
# Let's not leave this lying around.
admin.delete()
#=============================================================================================================
# Because these are expensive and far-reaching, I moved them here # Because these are expensive and far-reaching, I moved them here
def get_users_with_role(role_prefix): def get_users_with_role(role_prefix):
""" """
......
""" Management command to update courses' search index """ """ Management command to update courses' search index """
import logging import logging
from optparse import make_option
from textwrap import dedent from textwrap import dedent
from django.core.management import BaseCommand, CommandError from django.core.management import BaseCommand, CommandError
...@@ -22,32 +21,25 @@ class Command(BaseCommand): ...@@ -22,32 +21,25 @@ class Command(BaseCommand):
Examples: Examples:
./manage.py reindex_course <course_id_1> <course_id_2> - reindexes courses with keys course_id_1 and course_id_2 ./manage.py reindex_course <course_id_1> <course_id_2> ... - reindexes courses with provided keys
./manage.py reindex_course --all - reindexes all available courses ./manage.py reindex_course --all - reindexes all available courses
./manage.py reindex_course --setup - reindexes all courses for devstack setup ./manage.py reindex_course --setup - reindexes all courses for devstack setup
""" """
help = dedent(__doc__) help = dedent(__doc__)
can_import_settings = True can_import_settings = True
args = "<course_id course_id ...>"
all_option = make_option('--all',
action='store_true',
dest='all',
default=False,
help='Reindex all courses')
setup_option = make_option('--setup',
action='store_true',
dest='setup',
default=False,
help='Reindex all courses on developers stack setup')
option_list = BaseCommand.option_list + (all_option, setup_option)
CONFIRMATION_PROMPT = u"Re-indexing all courses might be a time consuming operation. Do you want to continue?" CONFIRMATION_PROMPT = u"Re-indexing all courses might be a time consuming operation. Do you want to continue?"
def add_arguments(self, parser):
parser.add_argument('course_ids',
nargs='*',
metavar='course_id')
parser.add_argument('--all',
action='store_true',
help='Reindex all courses')
parser.add_argument('--setup',
action='store_true',
help='Reindex all courses on developers stack setup')
def _parse_course_key(self, raw_value): def _parse_course_key(self, raw_value):
""" Parses course key from string """ """ Parses course key from string """
try: try:
...@@ -65,12 +57,14 @@ class Command(BaseCommand): ...@@ -65,12 +57,14 @@ class Command(BaseCommand):
By convention set by Django developers, this method actually executes command's actions. By convention set by Django developers, this method actually executes command's actions.
So, there could be no better docstring than emphasize this once again. So, there could be no better docstring than emphasize this once again.
""" """
all_option = options.get('all', False) course_ids = options['course_ids']
setup_option = options.get('setup', False) all_option = options['all']
setup_option = options['setup']
index_all_courses_option = all_option or setup_option index_all_courses_option = all_option or setup_option
if len(args) == 0 and not index_all_courses_option: if (not len(course_ids) and not index_all_courses_option) or \
raise CommandError(u"reindex_course requires one or more arguments: <course_id>") (len(course_ids) and index_all_courses_option):
raise CommandError("reindex_course requires one or more <course_id>s OR the --all or --setup flags.")
store = modulestore() store = modulestore()
...@@ -82,7 +76,7 @@ class Command(BaseCommand): ...@@ -82,7 +76,7 @@ class Command(BaseCommand):
# try getting the ElasticSearch engine # try getting the ElasticSearch engine
searcher = SearchEngine.get_search_engine(index_name) searcher = SearchEngine.get_search_engine(index_name)
except exceptions.ElasticsearchException as exc: except exceptions.ElasticsearchException as exc:
logging.exception('Search Engine error - %s', unicode(exc)) logging.exception('Search Engine error - %s', exc)
return return
index_exists = searcher._es.indices.exists(index=index_name) # pylint: disable=protected-access index_exists = searcher._es.indices.exists(index=index_name) # pylint: disable=protected-access
...@@ -108,7 +102,7 @@ class Command(BaseCommand): ...@@ -108,7 +102,7 @@ class Command(BaseCommand):
return return
else: else:
# in case course keys are provided as arguments # in case course keys are provided as arguments
course_keys = map(self._parse_course_key, args) course_keys = map(self._parse_course_key, course_ids)
for course_key in course_keys: for course_key in course_keys:
CoursewareSearchIndexer.do_course_reindex(store, course_key) CoursewareSearchIndexer.do_course_reindex(store, course_key)
""" Management command to update libraries' search index """ """ Management command to update libraries' search index """
from optparse import make_option from __future__ import print_function
from textwrap import dedent from textwrap import dedent
from django.core.management import BaseCommand, CommandError from django.core.management import BaseCommand, CommandError
...@@ -22,21 +22,17 @@ class Command(BaseCommand): ...@@ -22,21 +22,17 @@ class Command(BaseCommand):
./manage.py reindex_library --all - reindexes all available libraries ./manage.py reindex_library --all - reindexes all available libraries
""" """
help = dedent(__doc__) help = dedent(__doc__)
can_import_settings = True can_import_settings = True
CONFIRMATION_PROMPT = u"Reindexing all libraries might be a time consuming operation. Do you want to continue?"
args = "<library_id library_id ...>" def add_arguments(self, parser):
parser.add_argument('library_ids', nargs='*')
option_list = BaseCommand.option_list + ( parser.add_argument(
make_option(
'--all', '--all',
action='store_true', action='store_true',
dest='all', dest='all',
default=False,
help='Reindex all libraries' help='Reindex all libraries'
),) )
CONFIRMATION_PROMPT = u"Reindexing all libraries might be a time consuming operation. Do you want to continue?"
def _parse_library_key(self, raw_value): def _parse_library_key(self, raw_value):
""" Parses library key from string """ """ Parses library key from string """
...@@ -52,18 +48,19 @@ class Command(BaseCommand): ...@@ -52,18 +48,19 @@ class Command(BaseCommand):
By convention set by django developers, this method actually executes command's actions. By convention set by django developers, this method actually executes command's actions.
So, there could be no better docstring than emphasize this once again. So, there could be no better docstring than emphasize this once again.
""" """
if len(args) == 0 and not options.get('all', False): if (not options['library_ids'] and not options['all']) or (options['library_ids'] and options['all']):
raise CommandError(u"reindex_library requires one or more arguments: <library_id>") raise CommandError(u"reindex_library requires one or more <library_id>s or the --all flag.")
store = modulestore() store = modulestore()
if options.get('all', False): if options['all']:
if query_yes_no(self.CONFIRMATION_PROMPT, default="no"): if query_yes_no(self.CONFIRMATION_PROMPT, default="no"):
library_keys = [library.location.library_key.replace(branch=None) for library in store.get_libraries()] library_keys = [library.location.library_key.replace(branch=None) for library in store.get_libraries()]
else: else:
return return
else: else:
library_keys = map(self._parse_library_key, args) library_keys = map(self._parse_library_key, options['library_ids'])
for library_key in library_keys: for library_key in library_keys:
print("Indexing library {}".format(library_key))
LibrarySearchIndexer.do_library_reindex(store, library_key) LibrarySearchIndexer.do_library_reindex(store, library_key)
...@@ -6,8 +6,8 @@ from xmodule.contentstore.utils import restore_asset_from_trashcan ...@@ -6,8 +6,8 @@ from xmodule.contentstore.utils import restore_asset_from_trashcan
class Command(BaseCommand): class Command(BaseCommand):
help = '''Restore a deleted asset from the trashcan back to it's original course''' help = '''Restore a deleted asset from the trashcan back to it's original course'''
def handle(self, *args, **options): def add_arguments(self, parser):
if len(args) != 1 and len(args) != 0: parser.add_argument('location')
raise CommandError("restore_asset_from_trashcan requires one argument: <location>")
restore_asset_from_trashcan(args[0]) def handle(self, *args, **options):
restore_asset_from_trashcan(options['location'])
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import ddt import ddt
from django.core.management import call_command, CommandError from django.core.management import call_command, CommandError
import mock import mock
from six import text_type
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -60,34 +61,34 @@ class TestReindexCourse(ModuleStoreTestCase): ...@@ -60,34 +61,34 @@ class TestReindexCourse(ModuleStoreTestCase):
def test_given_library_key_raises_command_error(self): def test_given_library_key_raises_command_error(self):
""" Test that raises CommandError if library key is passed """ """ Test that raises CommandError if library key is passed """
with self.assertRaisesRegexp(CommandError, ".* is not a course key"): with self.assertRaisesRegexp(CommandError, ".* is not a course key"):
call_command('reindex_course', unicode(self._get_lib_key(self.first_lib))) call_command('reindex_course', text_type(self._get_lib_key(self.first_lib)))
with self.assertRaisesRegexp(CommandError, ".* is not a course key"): with self.assertRaisesRegexp(CommandError, ".* is not a course key"):
call_command('reindex_course', unicode(self._get_lib_key(self.second_lib))) call_command('reindex_course', text_type(self._get_lib_key(self.second_lib)))
with self.assertRaisesRegexp(CommandError, ".* is not a course key"): with self.assertRaisesRegexp(CommandError, ".* is not a course key"):
call_command( call_command(
'reindex_course', 'reindex_course',
unicode(self.second_course.id), text_type(self.second_course.id),
unicode(self._get_lib_key(self.first_lib)) text_type(self._get_lib_key(self.first_lib))
) )
def test_given_id_list_indexes_courses(self): def test_given_id_list_indexes_courses(self):
""" Test that reindexes courses when given single course key or a list of course keys """ """ Test that reindexes courses when given single course key or a list of course keys """
with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \
mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)):
call_command('reindex_course', unicode(self.first_course.id)) call_command('reindex_course', text_type(self.first_course.id))
self.assertEqual(patched_index.mock_calls, self._build_calls(self.first_course)) self.assertEqual(patched_index.mock_calls, self._build_calls(self.first_course))
patched_index.reset_mock() patched_index.reset_mock()
call_command('reindex_course', unicode(self.second_course.id)) call_command('reindex_course', text_type(self.second_course.id))
self.assertEqual(patched_index.mock_calls, self._build_calls(self.second_course)) self.assertEqual(patched_index.mock_calls, self._build_calls(self.second_course))
patched_index.reset_mock() patched_index.reset_mock()
call_command( call_command(
'reindex_course', 'reindex_course',
unicode(self.first_course.id), text_type(self.first_course.id),
unicode(self.second_course.id) text_type(self.second_course.id)
) )
expected_calls = self._build_calls(self.first_course, self.second_course) expected_calls = self._build_calls(self.first_course, self.second_course)
self.assertEqual(patched_index.mock_calls, expected_calls) self.assertEqual(patched_index.mock_calls, expected_calls)
...@@ -121,4 +122,4 @@ class TestReindexCourse(ModuleStoreTestCase): ...@@ -121,4 +122,4 @@ class TestReindexCourse(ModuleStoreTestCase):
patched_index.side_effect = SearchIndexingError("message", []) patched_index.side_effect = SearchIndexingError("message", [])
with self.assertRaises(SearchIndexingError): with self.assertRaises(SearchIndexingError):
call_command('reindex_course', unicode(self.second_course.id)) call_command('reindex_course', text_type(self.second_course.id))
""" """
Verify the structure of courseware as to it's suitability for import Verify the structure of courseware as to it's suitability for import
""" """
from django.core.management.base import BaseCommand, CommandError from __future__ import print_function
from argparse import REMAINDER
from django.core.management.base import BaseCommand
from xmodule.modulestore.xml_importer import perform_xlint from xmodule.modulestore.xml_importer import perform_xlint
class Command(BaseCommand): class Command(BaseCommand):
"""Verify the structure of courseware as to it's suitability for import""" """Verify the structure of courseware as to its suitability for import"""
help = "Verify the structure of courseware as to it's suitability for import" help = """
Verify the structure of courseware as to its suitability for import.
To run: manage.py cms <data directory> [<course dir>...]
"""
def add_arguments(self, parser):
parser.add_argument('data_dir')
parser.add_argument('source_dirs', nargs=REMAINDER)
def handle(self, *args, **options): def handle(self, *args, **options):
"Execute the command" """Execute the command"""
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]") data_dir = options['data_dir']
source_dirs = options['source_dirs']
data_dir = args[0]
if len(args) > 1:
source_dirs = args[1:]
else:
source_dirs = None
print("Importing. Data_dir={data}, source_dirs={courses}".format( print("Importing. Data_dir={data}, source_dirs={courses}".format(
data=data_dir, data=data_dir,
courses=source_dirs)) courses=source_dirs))
perform_xlint(data_dir, source_dirs, load_error_modules=False) perform_xlint(data_dir, source_dirs, load_error_modules=False)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment