def configure_content(instance, url): """ Configures course content by trusted remote URL. """ if not url: return [_("Configuration URL required.")] try: url = url.strip() response = requests.get(url) except Exception as e: return [ _("Request for a course configuration failed with error '{error!s}'. " "Configuration of course aborted.").format(error=e) ] instance.configure_url = url instance.save() try: config = json.loads(response.text) except Exception as e: return [ _("JSON parser raised error '{error!s}'. " "Configuration of course aborted.").format(error=e) ] errors = [] # Configure course instance attributes. if "start" in config: dt = parse_date(config["start"], errors) if dt: instance.starting_time = dt if "end" in config: dt = parse_date(config["end"], errors) if dt: instance.ending_time = dt if "enrollment_start" in config: instance.enrollment_starting_time = parse_date( config["enrollment_start"], errors, allow_null=True) if "enrollment_end" in config: instance.enrollment_ending_time = parse_date(config["enrollment_end"], errors, allow_null=True) if "lifesupport_time" in config: instance.lifesupport_time = parse_date(config["lifesupport_time"], errors, allow_null=True) if "archive_time" in config: instance.archive_time = parse_date(config["archive_time"], errors, allow_null=True) if "enrollment_audience" in config: enroll_audience = parse_choices( config["enrollment_audience"], { 'internal': CourseInstance.ENROLLMENT_AUDIENCE.INTERNAL_USERS, 'external': CourseInstance.ENROLLMENT_AUDIENCE.EXTERNAL_USERS, 'all': CourseInstance.ENROLLMENT_AUDIENCE.ALL_USERS, }, "enrollment_audience", errors) if enroll_audience is not None: instance.enrollment_audience = enroll_audience if "view_content_to" in config: view_content_to = parse_choices( config["view_content_to"], { 'enrolled': CourseInstance.VIEW_ACCESS.ENROLLED, 'enrollment_audience': CourseInstance.VIEW_ACCESS.ENROLLMENT_AUDIENCE, 'all_registered': CourseInstance.VIEW_ACCESS.ALL_REGISTERED, 'public': CourseInstance.VIEW_ACCESS.PUBLIC, }, "view_content_to", errors) if view_content_to is not None: instance.view_content_to = view_content_to if "index_mode" in config: index_mode = parse_choices( config["index_mode"], { 'results': CourseInstance.INDEX_TYPE.RESULTS, 'toc': CourseInstance.INDEX_TYPE.TOC, 'last': CourseInstance.INDEX_TYPE.LAST, 'experimental': CourseInstance.INDEX_TYPE.EXPERIMENT, }, "index_mode", errors) if index_mode is not None: instance.index_mode = index_mode numbering_choices = { 'none': CourseInstance.CONTENT_NUMBERING.NONE, 'arabic': CourseInstance.CONTENT_NUMBERING.ARABIC, 'roman': CourseInstance.CONTENT_NUMBERING.ROMAN, 'hidden': CourseInstance.CONTENT_NUMBERING.HIDDEN, } if "content_numbering" in config: numbering = parse_choices(config["content_numbering"], numbering_choices, "content_numbering", errors) if numbering is not None: instance.content_numbering = numbering if "module_numbering" in config: numbering = parse_choices(config["module_numbering"], numbering_choices, "module_numbering", errors) if numbering is not None: instance.module_numbering = numbering if "course_description" in config: # Course index.yaml files have previously used the field "description" # for a hidden description, so we use "course_description" for # the visible description. instance.description = str(config["course_description"]) if "course_footer" in config: instance.footer = str(config["course_footer"]) if "lang" in config: langs = config["lang"] if isinstance(langs, list): langs = [ lang for lang in langs if instance.is_valid_language(lang) ] if langs: instance.language = "|{}|".format("|".join(langs)) elif instance.is_valid_language(langs): instance.language = str(langs)[:5] if "contact" in config: instance.technical_error_emails = str(config["contact"]) if "head_urls" in config: head_urls = config["head_urls"] instance.head_urls = "\n".join(head_urls) if isinstance( head_urls, list) else str(head_urls) if "assistants" in config: if not isinstance(config["assistants"], list): errors.append(_("Assistants must be given as a student ID array.")) else: assistants = [] for sid in config["assistants"]: try: profile = UserProfile.objects.get(student_id=sid) except UserProfile.DoesNotExist as err: errors.append( _("Adding the assistant failed, because an associated " "user with student ID {id} does not exist.").format( id=sid)) else: assistants.append(profile) instance.assistants.set(assistants) instance.build_log_url = str( config['build_log_url']) if 'build_log_url' in config else '' # configure_url excluded from validation because the default Django URL # validation does not accept dotless domain names such as "grader" instance.full_clean(exclude=['configure_url']) instance.save() if not "categories" in config or not isinstance(config["categories"], dict): errors.append(_("Categories required as an object.")) return errors if not "modules" in config or not isinstance(config["modules"], list): errors.append(_("Modules required as an array.")) return errors # Configure learning object categories. category_map = {} seen = [] for key, c in config.get("categories", {}).items(): if not "name" in c: errors.append(_("Category requires a name.")) continue try: category = instance.categories.get( name=format_localization(c["name"])) except LearningObjectCategory.DoesNotExist: category = LearningObjectCategory(course_instance=instance, name=format_localization( c["name"])) if "status" in c: category.status = str(c["status"]) if "description" in c: category.description = str(c["description"]) if "points_to_pass" in c: i = parse_int(c["points_to_pass"], errors) if not i is None: category.points_to_pass = i for field in [ "confirm_the_level", "accept_unofficial_submits", ]: if field in c: setattr(category, field, parse_bool(c[field])) category.full_clean() category.save() category_map[key] = category seen.append(category.id) for category in instance.categories.all(): if not category.id in seen: category.status = LearningObjectCategory.STATUS.HIDDEN category.save() # Configure course modules. seen_modules = [] seen_objects = [] nn = 0 n = 0 for m in config.get("modules", []): if not "key" in m: errors.append(_("Module requires a key.")) continue try: module = instance.course_modules.get(url=str(m["key"])) except CourseModule.DoesNotExist: module = CourseModule(course_instance=instance, url=str(m["key"])) if "order" in m: module.order = parse_int(m["order"], errors) else: n += 1 module.order = n if "title" in m: module.name = format_localization(m["title"]) elif "name" in m: module.name = format_localization(m["name"]) if not module.name: module.name = "-" if "status" in m: module.status = str(m["status"]) if "points_to_pass" in m: i = parse_int(m["points_to_pass"], errors) if not i is None: module.points_to_pass = i if "introduction" in m: module.introduction = str(m["introduction"]) if "open" in m: dt = parse_date(m["open"], errors) if dt: module.opening_time = dt if not module.opening_time: module.opening_time = instance.starting_time if "close" in m: dt = parse_date(m["close"], errors) if dt: module.closing_time = dt elif "duration" in m: dt = parse_duration(module.opening_time, m["duration"], errors) if dt: module.closing_time = dt if not module.closing_time: module.closing_time = instance.ending_time if "read-open" in m: module.reading_opening_time = parse_date(m["read-open"], errors, allow_null=True) if "late_close" in m: dt = parse_date(m["late_close"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True elif "late_duration" in m: dt = parse_duration(module.closing_time, m["late_duration"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True if "late_penalty" in m: f = parse_float(m["late_penalty"], errors) if not f is None: module.late_submission_penalty = f module.full_clean() module.save() seen_modules.append(module.id) if not ("numerate_ignoring_modules" in config \ and parse_bool(config["numerate_ignoring_modules"])): nn = 0 if "children" in m: nn = configure_learning_objects(category_map, module, m["children"], None, seen_objects, errors, nn) for module in list(instance.course_modules.all()): if not module.id in seen_modules: module.status = CourseModule.STATUS.HIDDEN module.save() for lobject in list(module.learning_objects.all()): if not lobject.id in seen_objects: exercise = lobject.as_leaf_class() if (not isinstance(exercise, BaseExercise) or exercise.submissions.count() == 0): exercise.delete() else: lobject.status = LearningObject.STATUS.HIDDEN lobject.order = 9999 lobject.save() # Clean up obsolete categories. for category in instance.categories.filter( status=LearningObjectCategory.STATUS.HIDDEN): if category.learning_objects.count() == 0: category.delete() return errors
def main(): from django.contrib.auth.models import User from course.models import Course, CourseInstance, CourseModule from course.models import Enrollment, StudentGroup, LearningObjectCategory from exercise.exercise_models import BaseExercise from exercise.submission_models import Submission now = timezone.now() year_later = now + timedelta(days=365) user0 = User(id=500) user0.username = '******' user0.first_name = 'Perry' user0.last_name = 'Cash' user0.email = '*****@*****.**' user0.set_password('percash0') user0.save() user1 = User(id=501) user1.username = '******' user1.first_name = 'Zorita' user1.last_name = 'Alston' user1.email = '*****@*****.**' user1.set_password('zoralst1') user1.save() user2 = User(id=502) user2.username = '******' user2.first_name = 'Cameron' user2.last_name = 'Stein' user2.email = '*****@*****.**' user2.set_password('camstei2') user2.save() user3 = User(id=503) user3.username = '******' user3.first_name = 'Brynne' user3.last_name = 'Pollard' user3.email = '*****@*****.**' user3.set_password('brypoll3') user3.save() user4 = User(id=504) user4.username = '******' user4.first_name = 'Allistair' user4.last_name = 'Blackburn' user4.email = '*****@*****.**' user4.set_password('allblac4') user4.save() user5 = User(id=505) user5.username = '******' user5.first_name = 'Zachary' user5.last_name = 'Bolton' user5.email = '*****@*****.**' user5.set_password('zacbolt5') user5.save() user6 = User(id=506) user6.username = '******' user6.first_name = 'Kelsie' user6.last_name = 'Wolf' user6.email = '*****@*****.**' user6.set_password('kelwolf6') user6.save() user7 = User(id=507) user7.username = '******' user7.first_name = 'John' user7.last_name = 'McCarty' user7.email = '*****@*****.**' user7.set_password('johmcca7') user7.save() user8 = User(id=508) user8.username = '******' user8.first_name = 'Sheila' user8.last_name = 'Rodriquez' user8.email = '*****@*****.**' user8.set_password('sherodr8') user8.save() user9 = User(id=509) user9.username = '******' user9.first_name = 'Cassady' user9.last_name = 'Stanley' user9.email = '*****@*****.**' user9.set_password('casstan9') user9.save() course0 = Course() course0.name = 'Test Course' course0.url = 'test-course' course0.save() instance0 = CourseInstance(id=100, course=course0) instance0.instance_name = 'Test Instance' instance0.url = 'test-instance' instance0.starting_time = now instance0.ending_time = year_later instance0.save() Enrollment.objects.create(course_instance=instance0, user_profile=user0.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user1.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user2.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user3.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user4.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user5.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user6.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user7.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user8.userprofile) Enrollment.objects.create(course_instance=instance0, user_profile=user9.userprofile) group0 = StudentGroup.objects.create(id=200, course_instance=instance0) group0.members.add(user0.userprofile) group0.members.add(user1.userprofile) group0.save() group1 = StudentGroup.objects.create(id=201, course_instance=instance0) group1.members.add(user0.userprofile) group1.members.add(user2.userprofile) group1.members.add(user3.userprofile) group1.save() group2 = StudentGroup.objects.create(id=202, course_instance=instance0) group2.members.add(user1.userprofile) group2.members.add(user4.userprofile) group2.save() group3 = StudentGroup.objects.create(id=203, course_instance=instance0) group3.members.add(user5.userprofile) group3.members.add(user6.userprofile) group3.members.add(user7.userprofile) group3.members.add(user8.userprofile) group3.save() module0 = CourseModule(course_instance=instance0) module0.name = "First module" module0.url = "first-module" module0.opening_time = now module0.closing_time = year_later module0.save() module1 = CourseModule(course_instance=instance0) module1.name = "Second module" module1.url = "second-module" module1.opening_time = now module1.closing_time = year_later module1.save() category0 = LearningObjectCategory(course_instance=instance0) category0.name = "Some category" category0.save() exercise0 = BaseExercise(id=300, course_module=module0, category=category0) exercise0.name = "Easy exercise" exercise0.url = 'easy-exercise' exercise0.max_submissions = 10 exercise0.max_group_size = 4 exercise0.max_points = 100 exercise0.points_to_pass = 50 exercise0.save() exercise1 = BaseExercise(id=301, course_module=module0, category=category0) exercise1.name = "Hard exercise" exercise1.url = 'hard-exercise' exercise1.max_submissions = 5 exercise0.max_group_size = 2 exercise1.max_points = 100 exercise1.points_to_pass = 100 exercise1.save() exercise2 = BaseExercise(id=302, course_module=module1, category=category0) exercise2.name = "Nice exercise" exercise2.url = 'nice-exercise' exercise2.max_submissions = 0 exercise2.max_points = 10 exercise2.points_to_pass = 0 exercise2.save() submission0 = Submission.objects.create(id=400, exercise=exercise0) submission0.submitters.add(user0.userprofile) submission0.submitters.add(user1.userprofile) submission0.feedback = '<html><body>Not bad.</body></html>' submission0.set_points(40, 100) submission0.set_ready() submission0.save() submission1 = Submission.objects.create(id=401, exercise=exercise0) submission1.submitters.add(user0.userprofile) submission1.submitters.add(user1.userprofile) submission1.feedback = '<html><body>Good.</body></html>' submission1.set_points(60, 100) submission1.set_ready() submission1.save() submission2 = Submission.objects.create(id=402, exercise=exercise0) submission2.submitters.add(user1.userprofile) submission2.submitters.add(user4.userprofile) submission2.feedback = '<html><body>Good.</body></html>' submission2.set_points(50, 100) submission2.set_ready() submission2.save() submission3 = Submission.objects.create(id=403, exercise=exercise2) submission3.submitters.add(user0.userprofile) submission3.feedback = '<html><body>Excellent.</body></html>' submission3.set_points(10, 10) submission3.set_ready() submission3.save()
def configure_content(instance: CourseInstance, url: str) -> Tuple[bool, List[str]]: """ Configures course content by trusted remote URL. """ if not url: return False, [_('COURSE_CONFIG_URL_REQUIRED')] # save the url before fetching config. The JWT system requires this to be # set, so that A+ knows which service to trust to have access to the course # instance. The aplus config url might need access to the course instance. # The other service might also need to have access to the course instance # before it can be configured from the url. instance.configure_url = url instance.save() try: url = url.strip() permissions = Permissions() permissions.instances.add(Permission.READ, id=instance.id) permissions.instances.add(Permission.WRITE, id=instance.id) response = aplus_get(url, permissions=permissions) response.raise_for_status() except Exception as e: return False, [ format_lazy( _('COURSE_CONFIG_ERROR_REQUEST_FAILED -- {error!s}'), error=e, ) ] try: config = json.loads(response.text) except Exception as e: return False, [ format_lazy( _('COURSE_CONFIG_ERROR_JSON_PARSER_FAILED -- {error!s}'), error=e, ) ] if not isinstance(config, dict): return False, [_("COURSE_CONFIG_ERROR_INVALID_JSON")] errors = config.get('errors', []) if not isinstance(errors, list): errors = [str(errors)] if not config.get('success', True): errors.insert(0, _("COURSE_CONFIG_ERROR_SERVICE_FAILED_TO_EXPORT")) return False, errors # wrap everything in a transaction to make sure invalid configuration isn't saved with transaction.atomic(): # Configure course instance attributes. if "start" in config: dt = parse_date(config["start"], errors) if dt: instance.starting_time = dt if "end" in config: dt = parse_date(config["end"], errors) if dt: instance.ending_time = dt if "enrollment_start" in config: instance.enrollment_starting_time = parse_date( config["enrollment_start"], errors, allow_null=True) if "enrollment_end" in config: instance.enrollment_ending_time = parse_date( config["enrollment_end"], errors, allow_null=True) if "lifesupport_time" in config: instance.lifesupport_time = parse_date(config["lifesupport_time"], errors, allow_null=True) if "archive_time" in config: instance.archive_time = parse_date(config["archive_time"], errors, allow_null=True) if "enrollment_audience" in config: enroll_audience = parse_choices( config["enrollment_audience"], { 'internal': CourseInstance.ENROLLMENT_AUDIENCE.INTERNAL_USERS, 'external': CourseInstance.ENROLLMENT_AUDIENCE.EXTERNAL_USERS, 'all': CourseInstance.ENROLLMENT_AUDIENCE.ALL_USERS, }, "enrollment_audience", errors) if enroll_audience is not None: instance.enrollment_audience = enroll_audience if "view_content_to" in config: view_content_to = parse_choices( config["view_content_to"], { 'enrolled': CourseInstance.VIEW_ACCESS.ENROLLED, 'enrollment_audience': CourseInstance.VIEW_ACCESS.ENROLLMENT_AUDIENCE, 'all_registered': CourseInstance.VIEW_ACCESS.ALL_REGISTERED, 'public': CourseInstance.VIEW_ACCESS.PUBLIC, }, "view_content_to", errors) if view_content_to is not None: instance.view_content_to = view_content_to if "index_mode" in config: index_mode = parse_choices( config["index_mode"], { 'results': CourseInstance.INDEX_TYPE.RESULTS, 'toc': CourseInstance.INDEX_TYPE.TOC, 'last': CourseInstance.INDEX_TYPE.LAST, 'experimental': CourseInstance.INDEX_TYPE.EXPERIMENT, }, "index_mode", errors) if index_mode is not None: instance.index_mode = index_mode numbering_choices = { 'none': CourseInstance.CONTENT_NUMBERING.NONE, 'arabic': CourseInstance.CONTENT_NUMBERING.ARABIC, 'roman': CourseInstance.CONTENT_NUMBERING.ROMAN, 'hidden': CourseInstance.CONTENT_NUMBERING.HIDDEN, } if "content_numbering" in config: numbering = parse_choices(config["content_numbering"], numbering_choices, "content_numbering", errors) if numbering is not None: instance.content_numbering = numbering if "module_numbering" in config: numbering = parse_choices(config["module_numbering"], numbering_choices, "module_numbering", errors) if numbering is not None: instance.module_numbering = numbering if "course_description" in config: # Course index.yaml files have previously used the field "description" # for a hidden description, so we use "course_description" for # the visible description. instance.description = str(config["course_description"]) if "course_footer" in config: instance.footer = str(config["course_footer"]) if "lang" in config: langs = config["lang"] if isinstance(langs, list): langs = [ lang for lang in langs if instance.is_valid_language(lang) ] if langs: instance.language = "|{}|".format("|".join(langs)) elif instance.is_valid_language(langs): instance.language = str(langs)[:5] if "contact" in config: instance.technical_error_emails = str(config["contact"]) if "head_urls" in config: head_urls = config["head_urls"] instance.head_urls = "\n".join(head_urls) if isinstance( head_urls, list) else str(head_urls) if "assistants" in config: if not isinstance(config["assistants"], list): errors.append(_('COURSE_CONFIG_ERROR_ASSISTANTS_AS_SID_ARRAY')) else: assistants = [] for sid in config["assistants"]: try: profile = UserProfile.get_by_student_id(student_id=sid) except UserProfile.DoesNotExist as err: errors.append( format_lazy( _('COURSE_CONFIG_ERROR_ASSISTANT_NO_USER_WITH_SID -- {id}' ), id=sid, )) else: assistants.append(profile) instance.set_assistants(assistants) instance.build_log_url = str( config['build_log_url']) if 'build_log_url' in config else '' # configure_url excluded from validation because the default Django URL # validation does not accept dotless domain names such as "grader" instance.full_clean(exclude=['configure_url', 'build_log_url']) instance.save() if not "categories" in config or not isinstance( config["categories"], dict): errors.append(_('COURSE_CONFIG_ERROR_CATEGORIES_REQUIRED_OBJECT')) transaction.set_rollback(True) return False, errors if not "modules" in config or not isinstance(config["modules"], list): errors.append(_('COURSE_CONFIG_ERROR_MODULES_REQUIRED_ARRAY')) transaction.set_rollback(True) return False, errors # Configure learning object categories. category_map = {} seen = [] for key, c in config.get("categories", {}).items(): if not "name" in c: errors.append(_('COURSE_CONFIG_ERROR_CATEGORY_REQUIRES_NAME')) continue try: category = instance.categories.get( name=format_localization(c["name"])) except LearningObjectCategory.DoesNotExist: category = LearningObjectCategory(course_instance=instance, name=format_localization( c["name"])) if "status" in c: category.status = str(c["status"]) if "description" in c: category.description = str(c["description"]) if "points_to_pass" in c: i = parse_int(c["points_to_pass"], errors) if not i is None: category.points_to_pass = i for field in [ "confirm_the_level", "accept_unofficial_submits", ]: if field in c: setattr(category, field, parse_bool(c[field])) category.full_clean() category.save() category_map[key] = category seen.append(category.id) for category in instance.categories.all(): if not category.id in seen: category.status = LearningObjectCategory.STATUS.HIDDEN category.save() # Configure course modules. seen_modules = [] seen_objects = [] nn = 0 n = 0 for m in config.get("modules", []): if not "key" in m: errors.append(_('COURSE_CONFIG_ERROR_MODULE_REQUIRES_KEY')) continue try: module = instance.course_modules.get(url=str(m["key"])) except CourseModule.DoesNotExist: module = CourseModule(course_instance=instance, url=str(m["key"])) if "order" in m: module.order = parse_int(m["order"], errors) else: n += 1 module.order = n if "title" in m: module.name = format_localization(m["title"]) elif "name" in m: module.name = format_localization(m["name"]) if not module.name: module.name = "-" if "status" in m: module.status = str(m["status"]) if "points_to_pass" in m: i = parse_int(m["points_to_pass"], errors) if not i is None: module.points_to_pass = i if "introduction" in m: module.introduction = str(m["introduction"]) if "open" in m: dt = parse_date(m["open"], errors) if dt: module.opening_time = dt if not module.opening_time: module.opening_time = instance.starting_time if "close" in m: dt = parse_date(m["close"], errors) if dt: module.closing_time = dt elif "duration" in m: dt = parse_duration(module.opening_time, m["duration"], errors) if dt: module.closing_time = dt if not module.closing_time: module.closing_time = instance.ending_time if "read-open" in m: module.reading_opening_time = parse_date(m["read-open"], errors, allow_null=True) if "late_close" in m: dt = parse_date(m["late_close"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True elif "late_duration" in m: dt = parse_duration(module.closing_time, m["late_duration"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True if "late_penalty" in m: f = parse_float(m["late_penalty"], errors) if not f is None: module.late_submission_penalty = f module.full_clean() module.save() seen_modules.append(module.id) if not ("numerate_ignoring_modules" in config \ and parse_bool(config["numerate_ignoring_modules"])): nn = 0 if "children" in m: nn = configure_learning_objects(category_map, module, m["children"], None, seen_objects, errors, nn) for module in instance.course_modules.all(): # cache invalidation uses the parent when learning object is saved: # prefetch parent so that it wont be fetched after the it was deleted for lobject in module.learning_objects.all().prefetch_related( 'parent'): if lobject.id not in seen_objects: exercise = lobject.as_leaf_class() if (not isinstance(exercise, BaseExercise) or exercise.submissions.count() == 0): exercise.delete() else: lobject.status = LearningObject.STATUS.HIDDEN lobject.order = 9999 # .parent may have been deleted: only save status and order lobject.save(update_fields=["status", "order"]) if module.id not in seen_modules: if module.learning_objects.count() == 0: module.delete() else: module.status = CourseModule.STATUS.HIDDEN module.save() # Clean up obsolete categories. for category in instance.categories.filter( status=LearningObjectCategory.STATUS.HIDDEN): if category.learning_objects.count() == 0: category.delete() if "publish_url" in config: success = False publish_errors = [] try: permissions = Permissions() permissions.instances.add(Permission.READ, id=instance.id) permissions.instances.add(Permission.WRITE, id=instance.id) response = aplus_get(config["publish_url"], permissions=permissions) except ConnectionError as e: publish_errors = [str(e)] else: if response.status_code != 200: publish_errors = [ format_lazy( _("PUBLISH_RESPONSE_NON_200 -- {status_code}"), status_code=response.status_code) ] if response.text: try: publish_errors = json.loads(response.text) except Exception as e: publish_errors = [ format_lazy(_( "PUBLISH_ERROR_JSON_PARSER_FAILED -- {e}, {text}" ), e=e, text=response.text) ] else: if isinstance(publish_errors, dict): success = publish_errors.get("success", True) publish_errors = publish_errors.get("errors", []) if isinstance(publish_errors, list): publish_errors = (str(e) for e in publish_errors) else: publish_errors = [str(publish_errors)] if publish_errors: if not success: errors.append( format_lazy( _("PUBLISHED_WITH_ERRORS -- {publish_url}"), publish_url=config['publish_url'])) errors.extend(str(e) for e in publish_errors) if not success: transaction.set_rollback(True) return False, errors return True, errors
def configure_content(instance, url): """ Configures course content by trusted remote URL. """ if not url: return [_("Configuration URL required.")] try: url = url.strip() response = requests.get(url) except Exception as e: return [_("Request for a course configuration failed with error '{error!s}'. " "Configuration of course aborted.").format(error=e)] instance.configure_url = url instance.save() try: config = json.loads(response.text) except Exception as e: return [_("JSON parser raised error '{error!s}'. " "Configuration of course aborted.").format(error=e)] errors = [] # Configure course instance attributes. if "start" in config: dt = parse_date(config["start"], errors) if dt: instance.starting_time = dt if "end" in config: dt = parse_date(config["end"], errors) if dt: instance.ending_time = dt if "enrollment_start" in config: dt = parse_date(config["enrollment_start"], errors) if dt: instance.enrollment_starting_time = dt if "enrollment_end" in config: dt = parse_date(config["enrollment_end"], errors) if dt: instance.enrollment_ending_time = dt if "lifesupport_time" in config: dt = parse_date(config["lifesupport_time"], errors) if dt: instance.lifesupport_time = dt if "archive_time" in config: dt = parse_date(config["archive_time"], errors) if dt: instance.archive_time = dt if "enrollment_audience" in config: enroll_audience = parse_choices(config["enrollment_audience"], { 'internal': CourseInstance.ENROLLMENT_AUDIENCE.INTERNAL_USERS, 'external': CourseInstance.ENROLLMENT_AUDIENCE.EXTERNAL_USERS, 'all': CourseInstance.ENROLLMENT_AUDIENCE.ALL_USERS, }, "enrollment_audience", errors) if enroll_audience is not None: instance.enrollment_audience = enroll_audience if "view_content_to" in config: view_content_to = parse_choices(config["view_content_to"], { 'enrolled': CourseInstance.VIEW_ACCESS.ENROLLED, 'enrollment_audience': CourseInstance.VIEW_ACCESS.ENROLLMENT_AUDIENCE, 'all_registered': CourseInstance.VIEW_ACCESS.ALL_REGISTERED, 'public': CourseInstance.VIEW_ACCESS.PUBLIC, }, "view_content_to", errors) if view_content_to is not None: instance.view_content_to = view_content_to if "index_mode" in config: index_mode = parse_choices(config["index_mode"], { 'results': CourseInstance.INDEX_TYPE.RESULTS, 'toc': CourseInstance.INDEX_TYPE.TOC, 'last': CourseInstance.INDEX_TYPE.LAST, 'experimental': CourseInstance.INDEX_TYPE.EXPERIMENT, }, "index_mode", errors) if index_mode is not None: instance.index_mode = index_mode numbering_choices = { 'none': CourseInstance.CONTENT_NUMBERING.NONE, 'arabic': CourseInstance.CONTENT_NUMBERING.ARABIC, 'roman': CourseInstance.CONTENT_NUMBERING.ROMAN, 'hidden': CourseInstance.CONTENT_NUMBERING.HIDDEN, } if "content_numbering" in config: numbering = parse_choices(config["content_numbering"], numbering_choices, "content_numbering", errors) if numbering is not None: instance.content_numbering = numbering if "module_numbering" in config: numbering = parse_choices(config["module_numbering"], numbering_choices, "module_numbering", errors) if numbering is not None: instance.module_numbering = numbering if "course_description" in config: # Course index.yaml files have previously used the field "description" # for a hidden description, so we use "course_description" for # the visible description. instance.description = str(config["course_description"]) if "course_footer" in config: instance.footer = str(config["course_footer"]) if "lang" in config: langs = config["lang"] if isinstance(langs, list): langs = [lang for lang in langs if instance.is_valid_language(lang)] if langs: instance.language = "|{}|".format("|".join(langs)) elif instance.is_valid_language(langs): instance.language = str(langs)[:5] if "contact" in config: instance.technical_error_emails = str(config["contact"]) if "head_urls" in config: head_urls = config["head_urls"] instance.head_urls = "\n".join(head_urls) if isinstance(head_urls, list) else str(head_urls) if "assistants" in config: if not isinstance(config["assistants"], list): errors.append(_("Assistants must be given as a student ID array.")) else: assistants = [] for sid in config["assistants"]: try: profile = UserProfile.objects.get(student_id=sid) except UserProfile.DoesNotExist as err: errors.append(_("Adding the assistant failed, because an associated " "user with student ID {id} does not exist.").format(id=sid)) else: assistants.append(profile) instance.assistants.set(assistants) if "build_log_url" in config: instance.build_log_url = str(config["build_log_url"]) # configure_url excluded from validation because the default Django URL # validation does not accept dotless domain names such as "grader" instance.full_clean(exclude=['configure_url']) instance.save() if not "categories" in config or not isinstance(config["categories"], dict): errors.append(_("Categories required as an object.")) return errors if not "modules" in config or not isinstance(config["modules"], list): errors.append(_("Modules required as an array.")) return errors # Configure learning object categories. category_map = {} seen = [] for key, c in config.get("categories", {}).items(): if not "name" in c: errors.append(_("Category requires a name.")) continue try: category = instance.categories.get(name=format_localization(c["name"])) except LearningObjectCategory.DoesNotExist: category = LearningObjectCategory(course_instance=instance, name=format_localization(c["name"])) if "status" in c: category.status = str(c["status"])[:32] if "description" in c: category.description = str(c["description"]) if "points_to_pass" in c: i = parse_int(c["points_to_pass"], errors) if not i is None: category.points_to_pass = i for field in [ "confirm_the_level", "accept_unofficial_submits", ]: if field in c: setattr(category, field, parse_bool(c[field])) category.full_clean() category.save() category_map[key] = category seen.append(category.id) for category in instance.categories.all(): if not category.id in seen: category.status = 'hidden' category.save() # Configure course modules. seen_modules = [] seen_objects = [] nn = 0 n = 0 for m in config.get("modules", []): if not "key" in m: errors.append(_("Module requires a key.")) continue try: module = instance.course_modules.get(url=str(m["key"])) except CourseModule.DoesNotExist: module = CourseModule(course_instance=instance, url=str(m["key"])) if "order" in m: module.order = parse_int(m["order"], errors) else: n += 1 module.order = n if "title" in m: module.name = format_localization(m["title"]) elif "name" in m: module.name = format_localization(m["name"]) if not module.name: module.name = "-" if "status" in m: module.status = str(m["status"])[:32] if "points_to_pass" in m: i = parse_int(m["points_to_pass"], errors) if not i is None: module.points_to_pass = i if "introduction" in m: module.introduction = str(m["introduction"]) if "open" in m: dt = parse_date(m["open"], errors) if dt: module.opening_time = dt if not module.opening_time: module.opening_time = instance.starting_time if "close" in m: dt = parse_date(m["close"], errors) if dt: module.closing_time = dt elif "duration" in m: dt = parse_duration(module.opening_time, m["duration"], errors) if dt: module.closing_time = dt if not module.closing_time: module.closing_time = instance.ending_time if "late_close" in m: dt = parse_date(m["late_close"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True elif "late_duration" in m: dt = parse_duration(module.closing_time, m["late_duration"], errors) if dt: module.late_submission_deadline = dt module.late_submissions_allowed = True if "late_penalty" in m: f = parse_float(m["late_penalty"], errors) if not f is None: module.late_submission_penalty = f module.full_clean() module.save() seen_modules.append(module.id) if not ("numerate_ignoring_modules" in config \ and parse_bool(config["numerate_ignoring_modules"])): nn = 0 if "children" in m: nn = configure_learning_objects(category_map, module, m["children"], None, seen_objects, errors, nn) for module in list(instance.course_modules.all()): if not module.id in seen_modules: module.status = "hidden" module.save() for lobject in list(module.learning_objects.all()): if not lobject.id in seen_objects: exercise = lobject.as_leaf_class() if ( not isinstance(exercise, BaseExercise) or exercise.submissions.count() == 0 ): exercise.delete() else: lobject.status = "hidden" lobject.order = 9999 lobject.save() # Clean up obsolete categories. for category in instance.categories.filter(status="hidden"): if category.learning_objects.count() == 0: category.delete() return errors