Beispiel #1
0
def _load_text_from_node(node):
    """Load the text of a question.

    Args:
        node (dict): the information of the question to get the right text object for

    Returns:
        ybe.lib.ybe_contents.TextNode: the correct implementation of the question text
    """
    text_modes = {
        'text': Text,
        'text_markdown': TextMarkdown,
        'text_html': TextHTML
    }

    found_text_blocks = []
    found_text_modes = []
    for key, cls in text_modes.items():
        if key in node:
            found_text_blocks.append(cls(text=node[key]))
            found_text_modes.append(key)

    if len(found_text_blocks) == 0:
        raise YbeLoadingError('No text block defined in question.')
    elif len(found_text_blocks) > 1:
        raise YbeLoadingError(f'Multiple text blocks found {found_text_modes} in question.')
    return found_text_blocks[0]
Beispiel #2
0
def _load_points(value, must_be_set=False):
    """Load the points from the provided value.

    Args:
        value (object): the content of a ``points`` node.
        must_be_set: if True, the value must be set, if False, None is allowed

    Returns:
        int or float: the point value

    Raises:
        YbeLoadingError: if the value was not a float or int
    """
    if value is None:
        if must_be_set:
            raise YbeLoadingError(f'No points set, while points is a required field.')
        return None

    try:
        points = float(value)
    except ValueError:
        raise YbeLoadingError(f'Points should be a float, "{value}" given.')

    if points.is_integer():
        return int(points)

    return points
Beispiel #3
0
def _load_question(node):
    """Load the information of a single question.

    This infers the question type from the keyword ``type`` and loads the appropriate class.

    Args:
        node (dict): list of question information, the key should be the question type, the value should be the content.

    Returns:
        ybe.lib.ybe_contents.Question: the loaded question object, parsed from the provided information

    Raises:
        ybe.lib.errors.YbeLoadingError: if the information could not be loaded due to syntax errors
    """
    question_types = {
        'multiple_choice': _load_multiple_choice,
        'multiple_response': _load_multiple_response,
        'open': _load_open_question,
        'text_only': _load_text_only_question
    }

    question_type, question_content = list(node.items())[0]
    question_loader = question_types.get(question_type, None)

    if question_loader is None:
        raise YbeLoadingError('The requested question type {} was not recognized.'.format(question_type))

    return question_loader(question_content)
Beispiel #4
0
def _load_questions(node):
    """Load a list of questions.

    Args:
        node (List[dict]): list of question information, the key of each dict should be the question type,
            the value should be the content.

    Returns:
        List[Question]: list of question objects, parsed from the provided information
    """
    results = []
    exceptions = []
    for ind, question in enumerate(node):
        try:
            results.append(_load_question(question))
        except YbeLoadingError as ex:
            ex.question_ind = ind
            ex.question_id = list(question.values())[0].get('id')
            exceptions.append(ex)
            continue

    question_ids = [question.id for question in results]
    if len(question_ids) != len(set(question_ids)):
        duplicates = [item for item, count in collections.Counter(question_ids).items() if count > 1]
        exceptions.append(YbeLoadingError(f'There were multiple questions with the same id "{duplicates}"'))

    if len(exceptions):
        str(YbeMultipleLoadingErrors(exceptions))
        raise YbeMultipleLoadingErrors(exceptions)

    return results
Beispiel #5
0
def _load_meta_data_classification(node):
    """Load the classification meta data of a question.

    Args:
        node (dict): the content of the classification meta data node

    Returns:
        ybe.lib.ybe_contents.ClassificationQuestionMetaData: the question classification meta data
    """
    if not len(node):
        return ClassificationQuestionMetaData()

    exceptions = []

    related_concepts = node.get('related_concepts')
    if not (isinstance(related_concepts, list) or related_concepts is None):
        exceptions.append(YbeLoadingError(f'The value for ``meta_data.classification.related_concepts`` '
                                          f'should be a list, "{related_concepts}" given.'))

    skill_level = node.get('skill_level')
    skill_levels = ClassificationQuestionMetaData.available_skill_levels
    if skill_level not in skill_levels and skill_level is not None:
        exceptions.append(YbeLoadingError(f'The value for ``meta_data.classification.skill_level`` should be one of '
                                          f'"{skill_levels}", while "{skill_level}" was given.'))

    chapter = node.get('chapter')
    if not isinstance(chapter, int) and chapter is not None:
        exceptions.append(YbeLoadingError(f'The value for ``meta_data.classification.chapter`` should be an integer, '
                                          f'"{chapter}" was given.'))

    difficulty = node.get('difficulty')
    if (not isinstance(difficulty, int) or difficulty not in range(0, 11)) and difficulty is not None:
        exceptions.append(YbeLoadingError(f'The value for ``meta_data.classification.difficulty`` should be an '
                                          f'integer between [1-10], "{difficulty}" was given.'))

    if len(exceptions):
        raise YbeMultipleLoadingErrors(exceptions)

    return ClassificationQuestionMetaData(
        skill_level=skill_level,
        related_concepts=related_concepts,
        module=node.get('module'),
        chapter=chapter,
        difficulty=difficulty
    )
Beispiel #6
0
def _load_question_meta_data(node):
    """Load the meta data of a question.

    Args:
        node (dict): the information of the meta data

    Returns:
        ybe.lib.ybe_contents.QuestionMetaData: the meta data as an object.
    """
    keywords = node.get('general', {}).get('keywords')
    if not (isinstance(keywords, list) or keywords is None):
        raise YbeLoadingError(f'The value for ``meta_data.general.keywords`` should be a list, '
                              f'"{keywords}" was given.')

    return QuestionMetaData(
        general=GeneralQuestionMetaData(**node.get('general', {})),
        lifecycle=LifecycleQuestionMetaData(**node.get('lifecycle', {})),
        classification=_load_meta_data_classification(node.get('classification', {})),
        analytics=_load_meta_data_analytics(node.get('analytics', []))
    )
Beispiel #7
0
def read_ybe_string(ybe_str):
    """Load the data from the provided Ybe formatted string and return an :class:`ybe.lib.ybe_contents.YbeExam` object.

    Args:
        ybe_str (str): an .ybe formatted string to load

    Returns:
        ybe.lib.ybe_contents.YbeExam: the contents of the .ybe file.

    Raises:
        ybe.lib.errors.YbeLoadingError: if the file could not be loaded due to syntax errors
    """
    items = yaml.safe_load(ybe_str)

    if not len(items):
        return YbeExam()

    if 'ybe_version' not in items:
        raise YbeLoadingError('Missing "ybe_version" specifier.')

    return YbeExam(questions=_load_questions(items.get('questions', [])), info=YbeInfo(**items.get('info', {})))
Beispiel #8
0
def _load_multiple_choice_answers(node):
    """Load all the answers of a multiple choice question.

    Args:
        node (List[dict]): the list of answer items

    Returns:
        List[ybe.lib.ybe_contents.MultipleChoiceAnswer]: the multiple choice answers
    """
    exceptions = []

    answers = []
    for ind, item in enumerate(node):
        content = item['answer']
        answers.append(MultipleChoiceAnswer(
            text=_load_text_from_node(content),
            correct=content.get('correct', False)
        ))

    if not (s := sum(answer.correct for answer in answers)) == 1:
        exceptions.append(YbeLoadingError(f'A multiple choice question must have exactly '
                                          f'1 answer marked as correct, {s} marked.'))
Beispiel #9
0
def read_ybe_string(ybe_str):
    """Load the data from the provided Ybe formatted string and return an :class:`ybe.lib.ybe_contents.YbeExam` object.

    Args:
        ybe_str (str): an .ybe formatted string to load

    Returns:
        ybe.lib.ybe_contents.YbeExam: the contents of the .ybe file.

    Raises:
        ybe.lib.errors.YbeLoadingError: if the file could not be loaded due to syntax errors
    """
    yaml = YAML(typ='safe')
    yaml.register_class(TextHTML)
    yaml.register_class(TextMarkdown)
    data = yaml.load(ybe_str)

    if not len(data):
        return YbeExam()

    if 'ybe_version' not in data:
        raise YbeLoadingError('Missing "ybe_version" specifier.')

    def _convert_text(input_data):
        if isinstance(input_data, TextData):
            return input_data
        return TextPlain(input_data)

    def _convert_answer_options(input_data):
        result = []
        for item in input_data:
            if 'answer' in item:
                result.append(
                    dacite.from_dict(
                        AnswerOption,
                        item['answer'],
                        config=dacite.Config(
                            type_hooks={TextData: _convert_text})))
        return result

    def _convert_analytics(input_data):
        analytic_types = {
            'exam': QuestionUsedInExam,
        }
        result = []
        for usage in input_data:
            usage_type, usage_value = list(usage.items())[0]
            result.append(
                dacite.from_dict(analytic_types[usage_type],
                                 usage_value,
                                 config=dacite.Config(
                                     type_hooks={TextData: _convert_text})))
        return result

    def _convert_questions(input_data):
        question_types = {
            'multiple_choice': MultipleChoice,
            'multiple_response': MultipleResponse,
            'open': OpenQuestion,
            'text_only': TextOnly
        }
        result = []
        for question in input_data:
            q_type, q_value = list(question.items())[0]
            result.append(
                dacite.from_dict(
                    question_types[q_type],
                    q_value,
                    config=dacite.Config(
                        type_hooks={
                            TextData: _convert_text,
                            List[AnswerOption]: _convert_answer_options,
                            List[AnalyticsQuestionUsage]: _convert_analytics
                        })))
        return result

    return dacite.from_dict(
        YbeExam,
        data,
        config=dacite.Config(type_hooks={
            List[Question]: _convert_questions,
            TextData: _convert_text
        }))