Merge pull request #15697 from edx/LEARNER-2101
Add advanced settings to generate_courses command
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Django management command to generate a test course from a course config json
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from contentstore.management.commands.utils import user_from_str
|
||||
from contentstore.views.course import create_new_course_in_store
|
||||
from openedx.core.djangoapps.credit.models import CreditProvider
|
||||
from xmodule.course_module import CourseFields
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError
|
||||
from xmodule.tabs import CourseTabList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
""" Generate a basic course """
|
||||
help = 'Generate courses on studio from a json list of courses'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'courses_json',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
courses = json.loads(options["courses_json"])["courses"]
|
||||
except ValueError:
|
||||
raise CommandError("Invalid JSON object")
|
||||
except KeyError:
|
||||
raise CommandError("JSON object is missing courses list")
|
||||
|
||||
for course_settings in courses:
|
||||
# Validate course
|
||||
if not self._course_is_valid(course_settings):
|
||||
logger.warning("Can't create course, proceeding to next course")
|
||||
continue
|
||||
|
||||
# Retrieve settings
|
||||
org = course_settings["organization"]
|
||||
num = course_settings["number"]
|
||||
run = course_settings["run"]
|
||||
user_email = course_settings["user"]
|
||||
try:
|
||||
user = user_from_str(user_email)
|
||||
except User.DoesNotExist:
|
||||
logger.warning(user_email + " user does not exist")
|
||||
logger.warning("Can't create course, proceeding to next course")
|
||||
continue
|
||||
fields = self._process_course_fields(course_settings["fields"])
|
||||
|
||||
# Create the course
|
||||
try:
|
||||
new_course = create_new_course_in_store("split", user, org, num, run, fields)
|
||||
logger.info("Created {}".format(unicode(new_course.id)))
|
||||
except DuplicateCourseError:
|
||||
logger.warning("Course already exists for %s, %s, %s", org, num, run)
|
||||
|
||||
# Configure credit provider
|
||||
if ("enrollment" in course_settings) and ("credit_provider" in course_settings["enrollment"]):
|
||||
credit_provider = course_settings["enrollment"]["credit_provider"]
|
||||
if credit_provider is not None:
|
||||
CreditProvider.objects.get_or_create(
|
||||
provider_id=credit_provider,
|
||||
display_name=credit_provider
|
||||
)
|
||||
|
||||
def _process_course_fields(self, fields):
|
||||
""" Returns a validated list of course fields """
|
||||
all_fields = CourseFields.__dict__.keys()
|
||||
non_course_fields = [
|
||||
"__doc__",
|
||||
"__module__",
|
||||
"__weakref__",
|
||||
"__dict__"
|
||||
]
|
||||
for field in non_course_fields:
|
||||
all_fields.remove(field)
|
||||
|
||||
# Non-primitive course fields
|
||||
date_fields = [
|
||||
"certificate_available_date",
|
||||
"announcement",
|
||||
"enrollment_start",
|
||||
"enrollment_end",
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
course_tab_list_fields = [
|
||||
"tabs"
|
||||
]
|
||||
|
||||
for field in dict(fields):
|
||||
if field not in all_fields:
|
||||
# field does not exist as a CourseField
|
||||
del fields[field]
|
||||
logger.info(field + "is not a valid CourseField")
|
||||
elif fields[field] is None:
|
||||
# field is unset
|
||||
del fields[field]
|
||||
elif field in date_fields:
|
||||
# Generate Date object from the json value
|
||||
try:
|
||||
date_json = fields[field]
|
||||
fields[field] = Date().from_json(date_json)
|
||||
logger.info(field + " has been set to " + date_json)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.info("The date string could not be parsed for " + field)
|
||||
del fields[field]
|
||||
elif field in course_tab_list_fields:
|
||||
# Generate CourseTabList object from the json value
|
||||
try:
|
||||
course_tab_list_json = fields[field]
|
||||
fields[field] = CourseTabList().from_json(course_tab_list_json)
|
||||
logger.info(field + " has been set to " + course_tab_list_json)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.info("The course tab list string could not be parsed for " + field)
|
||||
del fields[field]
|
||||
else:
|
||||
# CourseField is valid and has been set
|
||||
logger.info(field + " has been set to " + str(fields[field]))
|
||||
|
||||
for field in all_fields:
|
||||
if field not in fields:
|
||||
logger.info(field + " has not been set")
|
||||
return fields
|
||||
|
||||
def _course_is_valid(self, course):
|
||||
""" Returns true if the course contains required settings """
|
||||
is_valid = True
|
||||
|
||||
# Check course settings
|
||||
required_course_settings = [
|
||||
"organization",
|
||||
"number",
|
||||
"run",
|
||||
"fields",
|
||||
"user"
|
||||
]
|
||||
for setting in required_course_settings:
|
||||
if setting not in course:
|
||||
logger.warning("Course json is missing " + setting)
|
||||
is_valid = False
|
||||
|
||||
# Check fields settings
|
||||
required_field_settings = [
|
||||
"display_name"
|
||||
]
|
||||
if "fields" in course:
|
||||
for setting in required_field_settings:
|
||||
if setting not in course["fields"]:
|
||||
logger.warning("Fields json is missing " + setting)
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
@@ -1,55 +0,0 @@
|
||||
"""
|
||||
Django management command to generate a test course in a specific modulestore
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from contentstore.management.commands.utils import user_from_str
|
||||
from contentstore.views.course import create_new_course_in_store
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
""" Generate a basic course """
|
||||
help = 'Generate a course with settings on studio'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'json',
|
||||
help='JSON object with values for store, user, name, organization, number, fields'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
if options["json"] is None:
|
||||
raise CommandError("Must pass in JSON object")
|
||||
|
||||
try:
|
||||
settings = json.loads(options["json"])
|
||||
except ValueError:
|
||||
raise CommandError("Invalid JSON")
|
||||
|
||||
if not(all(key in settings for key in ("store", "user", "organization", "number", "run", "fields"))):
|
||||
raise CommandError("JSON object is missing required fields")
|
||||
|
||||
if settings["store"] in [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]:
|
||||
store = settings["store"]
|
||||
else:
|
||||
raise CommandError("Modulestore invalid_store is not valid")
|
||||
|
||||
try:
|
||||
user = user_from_str(settings["user"])
|
||||
except User.DoesNotExist:
|
||||
raise CommandError("User {user} not found".format(user=settings["user"]))
|
||||
|
||||
org = settings["organization"]
|
||||
num = settings["number"]
|
||||
run = settings["run"]
|
||||
fields = settings["fields"]
|
||||
|
||||
# Create the course
|
||||
new_course = create_new_course_in_store(store, user, org, num, run, fields)
|
||||
self.stdout.write(u"Created {}".format(unicode(new_course.id)))
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Unittest for generate a test course in an given modulestore
|
||||
"""
|
||||
import json
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from django.core.management import CommandError, call_command
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestGenerateCourses(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for creating a course in split store via command line
|
||||
"""
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_generate_course_in_stores(self, mock_logger):
|
||||
"""
|
||||
Test that a course is created successfully
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course", "announcement": "2010-04-20T20:08:21.634121"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
key = modulestore().make_course_key("test-course-generator", "1", "1")
|
||||
self.assertTrue(modulestore().has_course(key))
|
||||
mock_logger.info.assert_any_call("Created course-v1:test-course-generator+1+1")
|
||||
mock_logger.info.assert_any_call("announcement has been set to 2010-04-20T20:08:21.634121")
|
||||
mock_logger.info.assert_any_call("display_name has been set to test-course")
|
||||
|
||||
def test_invalid_json(self):
|
||||
"""
|
||||
Test that providing an invalid JSON object will result in the appropriate command error
|
||||
"""
|
||||
with self.assertRaisesRegexp(CommandError, "Invalid JSON object"):
|
||||
arg = "invalid_json"
|
||||
call_command("generate_courses", arg)
|
||||
|
||||
def test_missing_courses_list(self):
|
||||
"""
|
||||
Test that a missing list of courses in json will result in the appropriate command error
|
||||
"""
|
||||
with self.assertRaisesRegexp(CommandError, "JSON object is missing courses list"):
|
||||
settings = {}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
@ddt.data("organization", "number", "run", "fields")
|
||||
def test_missing_course_settings(self, setting, mock_logger):
|
||||
"""
|
||||
Test that missing required settings in JSON object will result in the appropriate error message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course"}
|
||||
}]}
|
||||
del settings["courses"][0][setting]
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.warning.assert_any_call("Course json is missing " + setting)
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_invalid_user(self, mock_logger):
|
||||
"""
|
||||
Test that providing an invalid user in the course JSON will result in the appropriate error message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": "invalid_user",
|
||||
"fields": {"display_name": "test-course"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.warning.assert_any_call("invalid_user user does not exist")
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_missing_display_name(self, mock_logger):
|
||||
"""
|
||||
Test that missing required display_name in JSON object will result in the appropriate error message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.warning.assert_any_call("Fields json is missing display_name")
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_invalid_course_field(self, mock_logger):
|
||||
"""
|
||||
Test that an invalid course field will result in the appropriate message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course", "invalid_field": "invalid_value"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.info.assert_any_call((u'invalid_field') + "is not a valid CourseField")
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_invalid_date_setting(self, mock_logger):
|
||||
"""
|
||||
Test that an invalid date json will result in the appropriate message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course", "announcement": "invalid_date"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.info.assert_any_call("The date string could not be parsed for announcement")
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
def test_invalid_course_tab_list_setting(self, mock_logger):
|
||||
"""
|
||||
Test that an invalid course tab list json will result in the appropriate message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course", "tabs": "invalid_tabs"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.info.assert_any_call("The course tab list string could not be parsed for tabs")
|
||||
|
||||
@mock.patch('contentstore.management.commands.generate_courses.logger')
|
||||
@ddt.data("mobile_available", "enable_proctored_exams")
|
||||
def test_missing_course_fields(self, field, mock_logger):
|
||||
"""
|
||||
Test that missing course fields in fields json will result in the appropriate message
|
||||
"""
|
||||
settings = {"courses": [{
|
||||
"organization": "test-course-generator",
|
||||
"number": "1",
|
||||
"run": "1",
|
||||
"user": str(self.user.email),
|
||||
"fields": {"display_name": "test-course"}
|
||||
}]}
|
||||
arg = json.dumps(settings)
|
||||
call_command("generate_courses", arg)
|
||||
mock_logger.info.assert_any_call(field + " has not been set")
|
||||
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
Unittest for generate a test course in an given modulestore
|
||||
"""
|
||||
import unittest
|
||||
import ddt
|
||||
from django.core.management import CommandError, call_command
|
||||
|
||||
from contentstore.management.commands.generate_test_course import Command
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestGenerateTestCourse(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for creating a course in either old mongo or split mongo via command line
|
||||
"""
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_generate_course_in_stores(self, store):
|
||||
"""
|
||||
Test that courses are created successfully for both ModuleStores
|
||||
"""
|
||||
arg = (
|
||||
'{"store":"' + store + '",' +
|
||||
'"user":"' + self.user.email + '",' +
|
||||
'"organization":"test-course-generator",' +
|
||||
'"number":"1",' +
|
||||
'"run":"1",' +
|
||||
'"fields":{"display_name":"test-course"}}'
|
||||
)
|
||||
call_command("generate_test_course", arg)
|
||||
key = modulestore().make_course_key("test-course-generator", "1", "1")
|
||||
self.assertTrue(modulestore().has_course(key))
|
||||
|
||||
def test_invalid_json(self):
|
||||
"""
|
||||
Test that providing an invalid JSON object will result in the appropriate command error
|
||||
"""
|
||||
error_msg = "Invalid JSON"
|
||||
with self.assertRaisesRegexp(CommandError, error_msg):
|
||||
arg = "invalid_json"
|
||||
call_command("generate_test_course", arg)
|
||||
|
||||
def test_missing_fields(self):
|
||||
"""
|
||||
Test that missing required fields in JSON object will result in the appropriate command error
|
||||
"""
|
||||
error_msg = "JSON object is missing required fields"
|
||||
with self.assertRaisesRegexp(CommandError, error_msg):
|
||||
arg = (
|
||||
'{"store":"invalid_store",' +
|
||||
'"user":"user@example.com",' +
|
||||
'"organization":"test-course-generator"}'
|
||||
)
|
||||
call_command("generate_test_course", arg)
|
||||
|
||||
def test_invalid_store(self):
|
||||
"""
|
||||
Test that providing an invalid store option will result in the appropriate command error
|
||||
"""
|
||||
error_msg = "Modulestore invalid_store is not valid"
|
||||
with self.assertRaisesRegexp(CommandError, error_msg):
|
||||
arg = (
|
||||
'{"store":"invalid_store",' +
|
||||
'"user":"user@example.com",' +
|
||||
'"organization":"test-course-generator",' +
|
||||
'"number":"1",' +
|
||||
'"run":"1",' +
|
||||
'"fields":{"display_name":"test-course"}}'
|
||||
)
|
||||
call_command("generate_test_course", arg)
|
||||
|
||||
def test_invalid_user(self):
|
||||
"""
|
||||
Test that providing an invalid user will result in the appropriate command error
|
||||
"""
|
||||
error_msg = "User invalid_user not found"
|
||||
with self.assertRaisesRegexp(CommandError, error_msg):
|
||||
arg = (
|
||||
'{"store":"split",' +
|
||||
'"user":"invalid_user",' +
|
||||
'"organization":"test-course-generator",' +
|
||||
'"number":"1",' +
|
||||
'"run":"1",' +
|
||||
'"fields":{"display_name":"test-course"}}'
|
||||
)
|
||||
call_command("generate_test_course", arg)
|
||||
Reference in New Issue
Block a user