From d04c03e67c7be3b22fbd9bdc287be7213b54ab0e Mon Sep 17 00:00:00 2001 From: Steven Zheng Date: Tue, 1 Aug 2017 15:16:44 -0400 Subject: [PATCH] Add advanced settings to generate_courses command --- .../management/commands/generate_courses.py | 160 +++++++++++++++++ .../commands/generate_test_course.py | 55 ------ .../commands/tests/test_generate_courses.py | 170 ++++++++++++++++++ .../tests/test_generate_test_course.py | 89 --------- 4 files changed, 330 insertions(+), 144 deletions(-) create mode 100644 cms/djangoapps/contentstore/management/commands/generate_courses.py delete mode 100644 cms/djangoapps/contentstore/management/commands/generate_test_course.py create mode 100644 cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py delete mode 100644 cms/djangoapps/contentstore/management/commands/tests/test_generate_test_course.py diff --git a/cms/djangoapps/contentstore/management/commands/generate_courses.py b/cms/djangoapps/contentstore/management/commands/generate_courses.py new file mode 100644 index 0000000000..475e22801d --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/generate_courses.py @@ -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 diff --git a/cms/djangoapps/contentstore/management/commands/generate_test_course.py b/cms/djangoapps/contentstore/management/commands/generate_test_course.py deleted file mode 100644 index cd386aa24e..0000000000 --- a/cms/djangoapps/contentstore/management/commands/generate_test_course.py +++ /dev/null @@ -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))) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py new file mode 100644 index 0000000000..aa83b9d7ce --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py @@ -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") diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_generate_test_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_generate_test_course.py deleted file mode 100644 index 67dd8a1de4..0000000000 --- a/cms/djangoapps/contentstore/management/commands/tests/test_generate_test_course.py +++ /dev/null @@ -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)