Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
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
Beispiel #10
0
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