Commit 229c6858 by Steven Zheng Committed by GitHub

Merge pull request #15697 from edx/LEARNER-2101

Add advanced settings to generate_courses command
parents 55561990 d04c03e6
"""
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
"""
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)))
"""
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")
"""
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)
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