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 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_learning_objects(category_map, module, config, parent, seen, errors, n=0): if not isinstance(config, list): return n for o in config: if not "key" in o: errors.append(_("Learning object requires a key.")) continue if not "category" in o: errors.append(_("Learning object requires a category.")) continue if not o["category"] in category_map: errors.append( _("Unknown category '{category}'.").format( category=o["category"])) continue lobject = LearningObject.objects.filter( #course_module__course_instance=module.course_instance, course_module=module, url=str(o["key"])).defer(None).first() if not lobject is None: lobject = lobject.as_leaf_class() # Select exercise class. lobject_cls = ( LTIExercise if "lti" in o else ExerciseCollection if "target_category" in o else BaseExercise if "max_submissions" in o else CourseChapter) if not lobject is None and not isinstance(lobject, lobject_cls): lobject.url = lobject.url + "_old" lobject.save() lobject = None if lobject is None: lobject = lobject_cls(course_module=module, url=str(o["key"])) if lobject_cls == LTIExercise: lti = LTIService.objects.filter(menu_label=str(o["lti"])).first() if lti is None: errors.append( _("The site has no configuration for the LTI service '{lti_label}' " "used by the LTI exercise '{exercise_key}'. You may have misspelled " "the value for the field 'lti' or the site administrator has not yet " "added the configuration for the LTI service. The exercise was not " "created/updated due to the error.").format( lti_label=str(o["lti"]), exercise_key=str(o["key"]))) if hasattr(lobject, 'id'): # Avoid deleting LTI exercises from A+ since the LTI parameters # may have been used with an external tool. seen.append(lobject.id) # The learning object can not be saved without an LTI service # since the foreign key is required. continue else: lobject.lti_service = lti for key in [ "context_id", "resource_link_id", ]: obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, o[obj_key]) for key in ( "aplus_get_and_post", "open_in_iframe", ): obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, parse_bool(o[obj_key])) if lobject_cls in (LTIExercise, BaseExercise): for key in [ "allow_assistant_viewing", "allow_assistant_grading", ]: if key in o: setattr(lobject, key, parse_bool(o[key])) for key in [ "min_group_size", "max_group_size", "max_submissions", "max_points", "points_to_pass", ]: if key in o: i = parse_int(o[key], errors) if not i is None: setattr(lobject, key, i) if "difficulty" in o: lobject.difficulty = o["difficulty"] if lobject_cls == CourseChapter: if "generate_table_of_contents" in o: lobject.generate_table_of_contents = parse_bool( o["generate_table_of_contents"]) lobject.category = category_map[o["category"]] lobject.parent = parent if lobject_cls == ExerciseCollection: target_category, error_msg = get_target_category( o["target_category"], o["target_url"], ) if error_msg: errors.append("{} | {}".format(o["key"], error_msg)) continue if target_category.id == lobject.category.id: errors.append( "{} | ExerciseCollection can't target its own category". format(o["key"])) continue for key in [ "min_group_size", "max_group_size", "max_submissions", ]: if key in o: errors.append( "Can't define '{}' for ExerciseCollection".format(key)) if "max_points" in o and o["max_points"] <= 0: errors.append("ExerciseCollection can't have max_points <= 0") continue lobject.max_points = o['max_points'] lobject.points_to_pass = o['points_to_pass'] lobject.target_category = target_category lobject.min_group_size = 1 lobject.max_group_size = 1 lobject.max_submissions = 1 if "order" in o: lobject.order = parse_int(o["order"], errors) else: n += 1 lobject.order = n if "url" in o: lobject.service_url = format_localization(o["url"]) if "status" in o: lobject.status = str(o["status"]) if "audience" in o: words = {'internal': 1, 'external': 2, 'registered': 3} lobject.audience = words.get(o['audience'], 0) if "title" in o: lobject.name = format_localization(o["title"]) elif "name" in o: lobject.name = format_localization(o["name"]) if not lobject.name: lobject.name = "-" if "description" in o: lobject.description = str(o["description"]) if "use_wide_column" in o: lobject.use_wide_column = parse_bool(o["use_wide_column"]) if "exercise_info" in o: lobject.exercise_info = o["exercise_info"] if "model_answer" in o: lobject.model_answers = format_localization(o["model_answer"]) if "exercise_template" in o: lobject.templates = format_localization(o["exercise_template"]) lobject.full_clean() lobject.save() seen.append(lobject.id) if "children" in o: configure_learning_objects(category_map, module, o["children"], lobject, seen, errors) return n
def configure_learning_objects(category_map, module, config, parent, seen, errors, n=0): if not isinstance(config, list): return n for o in config: if not "key" in o: errors.append(_("Learning object requires a key.")) continue if not "category" in o: errors.append(_("Learning object requires a category.")) continue if not o["category"] in category_map: errors.append( _("Unknown category '{category}'.").format( category=o["category"])) continue lobject = LearningObject.objects.filter( #course_module__course_instance=module.course_instance, course_module=module, url=str(o["key"])).first() if not lobject is None: lobject = lobject.as_leaf_class() # Select exercise class. lobject_cls = (LTIExercise if "lti" in o else BaseExercise if "max_submissions" in o else CourseChapter) if not lobject is None and not isinstance(lobject, lobject_cls): lobject.url = lobject.url + "_old" lobject.save() lobject = None if lobject is None: lobject = lobject_cls(course_module=module, url=str(o["key"])) if lobject_cls == LTIExercise: lti = LTIService.objects.filter(menu_label=str(o["lti"])).first() if not lti is None: lobject.lti_service = lti for key in [ "context_id", "resource_link_id", ]: obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, o[obj_key]) for key in ( "aplus_get_and_post", "open_in_iframe", ): obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, parse_bool(o[obj_key])) if lobject_cls in (LTIExercise, BaseExercise): for key in [ "allow_assistant_viewing", "allow_assistant_grading", ]: if key in o: setattr(lobject, key, parse_bool(o[key])) for key in [ "min_group_size", "max_group_size", "max_submissions", "max_points", "points_to_pass", ]: if key in o: i = parse_int(o[key], errors) if not i is None: setattr(lobject, key, i) if "difficulty" in o: lobject.difficulty = o["difficulty"] if lobject_cls == CourseChapter: if "generate_table_of_contents" in o: lobject.generate_table_of_contents = parse_bool( o["generate_table_of_contents"]) lobject.category = category_map[o["category"]] lobject.parent = parent if "order" in o: lobject.order = parse_int(o["order"], errors) else: n += 1 lobject.order = n if "url" in o: lobject.service_url = format_localization(o["url"]) if "status" in o: lobject.status = str(o["status"])[:32] if "audience" in o: words = {'internal': 1, 'external': 2, 'registered': 3} lobject.audience = words.get(o['audience'], 0) if "title" in o: lobject.name = format_localization(o["title"]) elif "name" in o: lobject.name = format_localization(o["name"]) if not lobject.name: lobject.name = "-" if "description" in o: lobject.description = str(o["description"]) if "use_wide_column" in o: lobject.use_wide_column = parse_bool(o["use_wide_column"]) if "exercise_info" in o: lobject.exercise_info = o["exercise_info"] if "model_answer" in o: lobject.model_answers = format_localization(o["model_answer"]) if "exercise_template" in o: lobject.templates = format_localization(o["exercise_template"]) lobject.clean() lobject.save() seen.append(lobject.id) if "children" in o: configure_learning_objects(category_map, module, o["children"], lobject, seen, errors) return n
def configure_learning_objects( category_map: Dict[str, LearningObjectCategory], module: CourseModule, config: List[Dict[str, Any]], parent: Optional[LearningObject], seen: List[int], errors: List[str], n: int = 0, ) -> int: if not isinstance(config, list): return n for o in config: if not "key" in o: errors.append(_('LEARNING_OBJECT_ERROR_REQUIRES_KEY')) continue if not "category" in o: errors.append(_('LEARNING_OBJECT_ERROR_REQUIRES_CATEGORY')) continue if not o["category"] in category_map: errors.append( format_lazy( _('LEARNING_OBJECT_ERROR_UNKNOWN_CATEGORY -- {category}'), category=o["category"], )) continue lobject = LearningObject.objects.filter( #course_module__course_instance=module.course_instance, course_module=module, url=str(o["key"])).defer(None).first() if not lobject is None: lobject = lobject.as_leaf_class() # Select exercise class. lobject_cls = ( LTIExercise if "lti" in o else ExerciseCollection if "target_category" in o else BaseExercise if "max_submissions" in o else CourseChapter) if not lobject is None and not isinstance(lobject, lobject_cls): lobject.url = lobject.url + "_old" lobject.save() lobject = None if lobject is None: lobject = lobject_cls(course_module=module, url=str(o["key"])) if lobject_cls == LTIExercise: lti = LTIService.objects.filter(menu_label=str(o["lti"])).first() if lti is None: errors.append( format_lazy( _('LTI_ERROR_NO_CONFIGURATION_TO_SERVICE_USED_BY_EXERCISE -- {lti_label}, {exercise_key}' ), lti_label=str(o["lti"]), exercise_key=str(o["key"]), )) if hasattr(lobject, 'id'): # Avoid deleting LTI exercises from A+ since the LTI parameters # may have been used with an external tool. seen.append(lobject.id) # The learning object can not be saved without an LTI service # since the foreign key is required. continue else: lobject.lti_service = lti for key in [ "context_id", "resource_link_id", ]: obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, o[obj_key]) for key in ( "aplus_get_and_post", "open_in_iframe", ): obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, parse_bool(o[obj_key])) if lobject_cls in (LTIExercise, BaseExercise): for key in [ "allow_assistant_viewing", "allow_assistant_grading", ]: if key in o: setattr(lobject, key, parse_bool(o[key])) for key in [ "min_group_size", "max_group_size", "max_submissions", "max_points", "points_to_pass", ]: if key in o: i = parse_int(o[key], errors) if not i is None: setattr(lobject, key, i) if "difficulty" in o: lobject.difficulty = o["difficulty"] for config_key, lobject_key in [ ("reveal_submission_feedback", "submission_feedback_reveal_rule"), ("reveal_model_solutions", "model_solutions_reveal_rule"), ]: rule_config = o.get(config_key) if not rule_config: continue if not isinstance(rule_config, dict) or "trigger" not in rule_config: errors.append( format_lazy( _('REVEAL_RULE_ERROR_INVALID_JSON -- {key}'), key=config_key)) continue trigger = parse_choices( rule_config["trigger"], { "immediate": RevealRule.TRIGGER.IMMEDIATE, "manual": RevealRule.TRIGGER.MANUAL, "time": RevealRule.TRIGGER.TIME, "deadline": RevealRule.TRIGGER.DEADLINE, "deadline_all": RevealRule.TRIGGER.DEADLINE_ALL, "completion": RevealRule.TRIGGER.COMPLETION, }, "trigger", errors) rule = getattr(lobject, lobject_key) if not rule: rule = RevealRule() rule.trigger = trigger if "time" in rule_config: rule.time = parse_date(rule_config["time"], errors) if "delay_minutes" in rule_config: rule.delay_minutes = parse_int( rule_config["delay_minutes"], errors) rule.save() setattr(lobject, lobject_key, rule) if "grading_mode" in o: grading_mode = parse_choices( o["grading_mode"], { "best": BaseExercise.GRADING_MODE.BEST, "last": BaseExercise.GRADING_MODE.LAST, }, "grading_mode", errors) lobject.grading_mode = grading_mode else: # If not explicitly specified, grading mode is determined by # the submission feedback reveal rule. rule = lobject.submission_feedback_reveal_rule if rule and rule.trigger != RevealRule.TRIGGER.IMMEDIATE: lobject.grading_mode = BaseExercise.GRADING_MODE.LAST else: lobject.grading_mode = BaseExercise.GRADING_MODE.BEST if lobject_cls == CourseChapter: if "generate_table_of_contents" in o: lobject.generate_table_of_contents = parse_bool( o["generate_table_of_contents"]) lobject.category = category_map[o["category"]] lobject.parent = parent if lobject_cls == ExerciseCollection: target_category, error_msg = get_target_category( o["target_category"], o["target_url"], ) if error_msg: errors.append("{} | {}".format(o["key"], error_msg)) continue if target_category.id == lobject.category.id: errors.append( "{} | ExerciseCollection can't target its own category". format(o["key"])) continue for key in [ "min_group_size", "max_group_size", "max_submissions", ]: if key in o: errors.append( "Can't define '{}' for ExerciseCollection".format(key)) if "max_points" in o and o["max_points"] <= 0: errors.append("ExerciseCollection can't have max_points <= 0") continue lobject.max_points = o['max_points'] lobject.points_to_pass = o['points_to_pass'] lobject.target_category = target_category lobject.min_group_size = 1 lobject.max_group_size = 1 lobject.max_submissions = 1 if "order" in o: lobject.order = parse_int(o["order"], errors) else: n += 1 lobject.order = n if "url" in o: lobject.service_url = format_localization(o["url"]) if "status" in o: lobject.status = str(o["status"]) if "audience" in o: words = {'internal': 1, 'external': 2, 'registered': 3} lobject.audience = words.get(o['audience'], 0) if "title" in o: lobject.name = remove_newlines(format_localization(o["title"])) elif "name" in o: lobject.name = remove_newlines(format_localization(o["name"])) if not lobject.name: lobject.name = "-" if "description" in o: lobject.description = str(o["description"]) if "use_wide_column" in o: lobject.use_wide_column = parse_bool(o["use_wide_column"]) if "exercise_info" in o: lobject.exercise_info = o["exercise_info"] if "model_answer" in o: lobject.model_answers = format_localization(o["model_answer"]) if "exercise_template" in o: lobject.templates = format_localization(o["exercise_template"]) lobject.full_clean() lobject.save() seen.append(lobject.id) if "children" in o: configure_learning_objects(category_map, module, o["children"], lobject, seen, errors) return n
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 "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 "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"]) instance.clean() 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 category, flag = instance.categories.get_or_create( 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(o[field])) category.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 module, flag = instance.course_modules.get_or_create(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.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
def configure_learning_objects(category_map, module, config, parent, seen, errors, n=0): if not isinstance(config, list): return n for o in config: if not "key" in o: errors.append(_("Learning object requires a key.")) continue if not "category" in o: errors.append(_("Learning object requires a category.")) continue if not o["category"] in category_map: errors.append(_("Unknown category '{category}'.").format(category=o["category"])) continue lobject = LearningObject.objects.filter( #course_module__course_instance=module.course_instance, course_module=module, url=str(o["key"]) ).first() if not lobject is None: lobject = lobject.as_leaf_class() # Select exercise class. lobject_cls = ( LTIExercise if "lti" in o else BaseExercise if "max_submissions" in o else CourseChapter ) if not lobject is None and not isinstance(lobject, lobject_cls): lobject.url = lobject.url + "_old" lobject.save() lobject = None if lobject is None: lobject = lobject_cls(course_module=module, url=str(o["key"])) if lobject_cls == LTIExercise: lti = LTIService.objects.filter(menu_label=str(o["lti"])).first() if not lti is None: lobject.lti_service = lti for key in [ "context_id", "resource_link_id", ]: obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, o[obj_key]) for key in ( "aplus_get_and_post", "open_in_iframe", ): obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, parse_bool(o[obj_key])) if lobject_cls in (LTIExercise, BaseExercise): for key in [ "allow_assistant_viewing", "allow_assistant_grading", ]: if key in o: setattr(lobject, key, parse_bool(o[key])) for key in [ "min_group_size", "max_group_size", "max_submissions", "max_points", "points_to_pass", ]: if key in o: i = parse_int(o[key], errors) if not i is None: setattr(lobject, key, i) if "difficulty" in o: lobject.difficulty = o["difficulty"] if lobject_cls == CourseChapter: if "generate_table_of_contents" in o: lobject.generate_table_of_contents = parse_bool( o["generate_table_of_contents"]) lobject.category = category_map[o["category"]] lobject.parent = parent if "order" in o: lobject.order = parse_int(o["order"], errors) else: n += 1 lobject.order = n if "url" in o: lobject.service_url = format_localization(o["url"]) if "status" in o: lobject.status = str(o["status"])[:32] if "audience" in o: words = { 'internal':1, 'external':2, 'registered':3 } lobject.audience = words.get(o['audience'], 0) if "title" in o: lobject.name = format_localization(o["title"]) elif "name" in o: lobject.name = format_localization(o["name"]) if not lobject.name: lobject.name = "-" if "description" in o: lobject.description = str(o["description"]) if "use_wide_column" in o: lobject.use_wide_column = parse_bool(o["use_wide_column"]) if "exercise_info" in o: lobject.exercise_info = o["exercise_info"] if "model_answer" in o: lobject.model_answers = format_localization(o["model_answer"]) if "exercise_template" in o: lobject.templates = format_localization(o["exercise_template"]) lobject.clean() lobject.save() seen.append(lobject.id) if "children" in o: configure_learning_objects(category_map, module, o["children"], lobject, seen, errors) return n
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 "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 "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"]) instance.clean() 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 category, flag = instance.categories.get_or_create(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(o[field])) category.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 module, flag = instance.course_modules.get_or_create(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.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
def configure_learning_objects(category_map, module, config, parent, seen, errors, n=0): if not isinstance(config, list): return n for o in config: if not "key" in o: errors.append(_("Learning object requires a key.")) continue if not "category" in o: errors.append(_("Learning object requires a category.")) continue if not o["category"] in category_map: errors.append(_("Unknown category '{category}'.").format(category=o["category"])) continue lobject = LearningObject.objects.filter( #course_module__course_instance=module.course_instance, course_module=module, url=str(o["key"]) ).first() if not lobject is None: lobject = lobject.as_leaf_class() # Select exercise class. lobject_cls = ( LTIExercise if "lti" in o else ExerciseCollection if "collection_category" in o else BaseExercise if "max_submissions" in o else CourseChapter ) if not lobject is None and not isinstance(lobject, lobject_cls): lobject.url = lobject.url + "_old" lobject.save() lobject = None if lobject is None: lobject = lobject_cls(course_module=module, url=str(o["key"])) if lobject_cls == LTIExercise: lti = LTIService.objects.filter(menu_label=str(o["lti"])).first() if lti is None: errors.append( _("The site has no configuration for the LTI service '{lti_label}' " "used by the LTI exercise '{exercise_key}'. You may have misspelled " "the value for the field 'lti' or the site administrator has not yet " "added the configuration for the LTI service. The exercise was not " "created/updated due to the error.") .format(lti_label=str(o["lti"]), exercise_key=str(o["key"])) ) if hasattr(lobject, 'id'): # Avoid deleting LTI exercises from A+ since the LTI parameters # may have been used with an external tool. seen.append(lobject.id) # The learning object can not be saved without an LTI service # since the foreign key is required. continue else: lobject.lti_service = lti for key in [ "context_id", "resource_link_id", ]: obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, o[obj_key]) for key in ( "aplus_get_and_post", "open_in_iframe", ): obj_key = "lti_" + key if obj_key in o: setattr(lobject, key, parse_bool(o[obj_key])) if lobject_cls in (LTIExercise, BaseExercise): for key in [ "allow_assistant_viewing", "allow_assistant_grading", ]: if key in o: setattr(lobject, key, parse_bool(o[key])) for key in [ "min_group_size", "max_group_size", "max_submissions", "max_points", "points_to_pass", ]: if key in o: i = parse_int(o[key], errors) if not i is None: setattr(lobject, key, i) if "difficulty" in o: lobject.difficulty = o["difficulty"] if lobject_cls == CourseChapter: if "generate_table_of_contents" in o: lobject.generate_table_of_contents = parse_bool( o["generate_table_of_contents"]) lobject.category = category_map[o["category"]] lobject.parent = parent if lobject_cls == ExerciseCollection: if 'collection_course' in o and not o['collection_course'] is None: target_category, error_msg = get_target_category(o["collection_category"], course=o["collection_course"],) else: target_category, error_msg = get_target_category(o["collection_category"], course_url=o["collection_url"], ) if error_msg: errors.append("{} | {}".format(o["key"], error_msg)) continue if target_category.id == lobject.category.id: errors.append("ExerciseCollection can't target its own category") continue for key in [ "min_group_size", "max_group_size", "max_submissions", ]: if key in o: errors.append("Can't define '{}' for ExerciseCollection".format(key)) if "max_points" in o and o["max_points"] <= 0: errors.append("ExerciseCollection can't have max_points <= 0") continue lobject.target_category = target_category lobject.min_group_size = 1 lobject.max_group_size = 1 lobject.max_submissions = 1 if "order" in o: lobject.order = parse_int(o["order"], errors) else: n += 1 lobject.order = n if "url" in o: lobject.service_url = format_localization(o["url"]) if "status" in o: lobject.status = str(o["status"])[:32] if "audience" in o: words = { 'internal':1, 'external':2, 'registered':3 } lobject.audience = words.get(o['audience'], 0) if "title" in o: lobject.name = format_localization(o["title"]) elif "name" in o: lobject.name = format_localization(o["name"]) if not lobject.name: lobject.name = "-" if "description" in o: lobject.description = str(o["description"]) if "use_wide_column" in o: lobject.use_wide_column = parse_bool(o["use_wide_column"]) if "exercise_info" in o: lobject.exercise_info = o["exercise_info"] if "model_answer" in o: lobject.model_answers = format_localization(o["model_answer"]) if "exercise_template" in o: lobject.templates = format_localization(o["exercise_template"]) lobject.full_clean() lobject.save() seen.append(lobject.id) if "children" in o: configure_learning_objects(category_map, module, o["children"], lobject, seen, errors) return n
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