Exemple #1
0
    def create_question(self, title_text=None, points=True):
        env = self.state.document.settings.env
        env.question_count += 1

        # Create base element and data.
        node = aplus_nodes.html(u'div', {
            u'class': u' '.join(self.get_classes()),
        })
        data = {
            u'type': self.grader_field_type(),
            u'extra_info': self.get_extra_info(),
        }
        key = self.options.get('key', None)
        if key:
            data[u'key'] = yaml_writer.ensure_unicode(key)

        # Add title.
        if not title_text is None:
            data[u'title'] = title_text
        elif env.questionnaire_is_feedback:
            data[u'title'] = title_text = u''
        else:
            data[u'title|i18n'] = translations.opt('question',
                                                   postfix=u" {:d}".format(
                                                       env.question_count))
            title_text = u"{} {:d}".format(translations.get(env, 'question'),
                                           env.question_count)
        if title_text:
            title = aplus_nodes.html(u'label', {})
            title.append(nodes.Text(title_text))
            node.append(title)

        # Add configuration.
        if points and len(self.arguments) > 0:
            question_points = int(self.arguments[0])
            data['points'] = question_points
            env.aplus_quiz_total_points += question_points
            if env.aplus_pick_randomly_quiz:
                if env.aplus_single_question_points is None:
                    env.aplus_single_question_points = question_points
                else:
                    if env.aplus_single_question_points != question_points:
                        source, line = self.state_machine.get_source_and_line(
                            self.lineno)
                        logger.warning(
                            "Each question must have equal points when "
                            "the questionnaire uses the 'pick randomly' option.",
                            location=(source, line))

        if 'required' in self.options:
            data[u'required'] = True
        node.set_yaml(data, u'question')

        return env, node, data
    def create_question(self, title_text=None, points=True):
        env = self.state.document.settings.env
        env.question_count += 1

        # Create base element and data.
        node = aplus_nodes.html(u'div', {
            u'class': u' '.join(self.get_classes()),
        })
        data = {
            u'type': self.grader_field_type(),
            u'extra_info': self.get_extra_info(),
        }
        key = self.options.get('key', None)
        if key:
            data[u'key'] = yaml_writer.ensure_unicode(key)

        # Add title.
        if not title_text is None:
            data[u'title'] = title_text
        elif env.questionnaire_is_feedback:
            data[u'title'] = title_text = u''
        else:
            data[u'title|i18n'] = translations.opt('question',
                                                   postfix=u" {:d}".format(
                                                       env.question_count))
            title_text = u"{} {:d}".format(translations.get(env, 'question'),
                                           env.question_count)
        if title_text:
            title = aplus_nodes.html(u'label', {})
            title.append(nodes.Text(title_text))
            node.append(title)

        # Add configuration.
        if points and len(self.arguments) > 0:
            env.aplus_quiz_total_points += int(self.arguments[0])
            data[u'points'] = int(self.arguments[0])
        if 'required' in self.options:
            data[u'required'] = True
        node.set_yaml(data, u'question')

        return env, node, data
    def create_question(self, title_text=None, points=True):
        env = self.state.document.settings.env
        env.question_count += 1

        # Create base element and data.
        node = aplus_nodes.html(u'div', {
            u'class': u' '.join(self.get_classes()),
        })
        data = {
            u'type': self.grader_field_type(),
            u'extra_info': self.get_extra_info(),
        }
        key = self.options.get('key', None)
        if key:
            data[u'key'] = yaml_writer.ensure_unicode(key)

        # Add title.
        if not title_text is None:
            data[u'title'] = title_text
        elif env.questionnaire_is_feedback:
            data[u'title'] = title_text = u''
        else:
            data[u'title|i18n'] = translations.opt('question', postfix=u" {:d}".format(env.question_count))
            title_text = u"{} {:d}".format(translations.get(env, 'question'), env.question_count)
        if title_text:
            title = aplus_nodes.html(u'label', {})
            title.append(nodes.Text(title_text))
            node.append(title)

        # Add configuration.
        if points and len(self.arguments) > 0:
            data[u'points'] = int(self.arguments[0])
        if 'required' in self.options:
            data[u'required'] = True
        node.set_yaml(data, u'question')

        return env, node, data
    def parse_chapter(docname, doc, parent):
        for config_file in [
                e.yaml_write for e in doc.traverse(aplus_nodes.html)
                if e.has_yaml(u'exercise')
        ]:
            config = yaml_writer.read(config_file)
            if config.get(u'_external', False):
                exercise = config.copy()
                del exercise[u'_external']
            else:
                exercise = {
                    u'key':
                    config[u'key'],
                    u'config':
                    config[u'key'] + u'.yaml',
                    u'max_submissions':
                    config.get(u'max_submissions', 0),
                    u'max_points':
                    config.get(u'max_points', 0),
                    u'difficulty':
                    config.get(u'difficulty', ''),
                    u'points_to_pass':
                    config.get(u'points_to_pass', 0),
                    u'category':
                    config[u'category'],
                    u'min_group_size':
                    config.get(u'min_group_size', 1),
                    u'max_group_size':
                    config.get(u'max_group_size', 1),
                    u'confirm_the_level':
                    config.get(u'confirm_the_level', False),
                    u'allow_assistant_grading':
                    config.get(u'allow_assistant_grading', False),
                }
            exercise.update({
                u'status': u'unlisted',
            })
            if u'scale_points' in config:
                exercise[u'max_points'] = config.pop(u'scale_points')
            parent.append(exercise)
            if not config[u'category'] in category_keys:
                category_keys.append(config[u'category'])

        category = u'chapter'
        for name, hidden, child in traverse_tocs(app, doc):
            meta = first_meta(child)
            status = u'hidden' if 'hidden' in meta else (
                u'unlisted' if hidden else u'ready')
            chapter = {
                u'key': name.split(u'/')[-1],  #name.replace('/', '_'),
                u'status': status,
                u'name': first_title(child),
                u'static_content': name + u'.html',
                u'category': category,
                u'use_wide_column': app.config.use_wide_column,
                u'children': [],
            }
            if meta:
                audience = meta.get('audience')
                if audience:
                    chapter[u'audience'] = yaml_writer.ensure_unicode(audience)
            if category in override:
                chapter.update(override[category])
            parent.append(chapter)
            if not u'chapter' in category_keys:
                category_keys.append(u'chapter')
            parse_chapter(name, child, chapter[u'children'])
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
            u'data-aplus-active-element': u'out',
            u'data-inputs': u''+ self.options.get('inputs', ''),
        }

        if 'inputs' not in self.options:
            raise self.warning("The input list for output '{:s}' is empty.".format(key))

        if 'type' in self.options:
            args['data-type'] = self.options['type']
        else:
            args['data-type'] = 'text'

        if 'scale-size' in self.options:
            args['data-scale'] = ''

        if 'title' in self.options:
            args['data-title'] = self.options['title']

        if 'width' in self.options:
            args['style'] = 'width:'+ self.options['width'] + ';'

        if 'height' in self.options:
            if 'style' not in args:
                args['style'] = 'height:'+ self.options['height'] + ';'
            else:
                args['style'] = args['style'] + 'height:'+ self.options['height'] + ';'

        if 'clear' in self.options:
            args['style'] = args['style'] + 'clear:'+ self.options['clear'] + ';'

        node = aplus_nodes.html(u'div', args)
        paragraph = aplus_nodes.html(u'p', {})
        paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder')))
        node.append(paragraph)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get(u'title', None)
        else:
            data = { u'_external': True }
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            config_title = None

        config_title = self.options.get('title', config_title)

        category = u'submit'
        data.update({
            u'key': name,
            u'title': env.config.submit_title.format(
                key_title=key_title, config_title=config_title
            ),
            u'category': u'active elements',
            u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.ae_default_submissions)),
        })

        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        node.write_yaml(env, name, data, 'exercise')

        return [node]
Exemple #6
0
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])
        if difficulty:
            classes.append(u'difficulty-' + difficulty)

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
        }
        if 'quiz' in self.options:
            args[u'data-aplus-quiz'] = u'yes'
        if 'ajax' in self.options:
            args[u'data-aplus-ajax'] = u'yes'
        node = aplus_nodes.html(u'div', args)
        paragraph = aplus_nodes.html(u'p', {})
        paragraph.append(
            nodes.Text(translations.get(env, 'submit_placeholder')))
        node.append(paragraph)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(
                    self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get(u'title', None)
        else:
            data = {u'_external': True}
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            if 'lti' in self.options:
                data.update({
                    u'lti':
                    ensure_unicode(self.options['lti']),
                    u'lti_context_id':
                    ensure_unicode(self.options.get('lti_context_id', u'')),
                    u'lti_resource_link_id':
                    ensure_unicode(
                        self.options.get('lti_resource_link_id', u'')),
                })
            config_title = None

        config_title = self.options.get('title', config_title)
        if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options:
            data[u'radar_info'] = {
                u'tokenizer':
                self.options.get("radar_tokenizer"),
                u'minimum_match_tokens':
                self.options.get("radar_minimum_match_tokens"),
            }

        category = u'submit'
        data.update({
            u'key':
            name,
            u'title':
            env.config.submit_title.format(key_title=key_title,
                                           config_title=config_title),
            u'category':
            u'submit',
            u'scale_points':
            points,
            u'difficulty':
            difficulty or '',
            u'max_submissions':
            self.options.get(
                'submissions',
                data.get('max_submissions',
                         env.config.program_default_submissions)),
            u'min_group_size':
            data.get('min_group_size', env.config.default_min_group_size),
            u'max_group_size':
            data.get('max_group_size', env.config.default_max_group_size),
            u'points_to_pass':
            self.options.get('points-to-pass', data.get('points_to_pass', 0)),
        })
        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        if 'category' in self.options:
            data['category'] = str(self.options['category'])

        node.write_yaml(env, name, data, 'exercise')

        return [node]
Exemple #7
0
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
            u'data-aplus-active-element': u'out',
            u'data-inputs': u''+ self.options.get('inputs', ''),
        }

        if 'inputs' not in self.options:
            raise self.warning("The input list for output '{:s}' is empty.".format(key))

        if 'type' in self.options:
            args['data-type'] = self.options['type']
        else:
            args['data-type'] = 'text'

        if 'scale-size' in self.options:
            args['data-scale'] = ''

        if 'title' in self.options:
            args['data-title'] = self.options['title']

        if 'width' in self.options:
            args['style'] = 'width:'+ self.options['width'] + ';'

        if 'height' in self.options:
            if 'style' not in args:
                args['style'] = 'height:'+ self.options['height'] + ';'
            else:
                args['style'] = args['style'] + 'height:'+ self.options['height'] + ';'

        if 'clear' in self.options:
            args['style'] = args['style'] + 'clear:'+ self.options['clear'] + ';'

        node = aplus_nodes.html(u'div', args)
        paragraph = aplus_nodes.html(u'p', {})
        paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder')))
        node.append(paragraph)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get(u'title', None)
        else:
            data = { u'_external': True }
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            config_title = None

        config_title = self.options.get('title', config_title)

        category = u'submit'
        data.update({
            u'key': name,
            u'title': env.config.submit_title.format(
                key_title=key_title, config_title=config_title
            ),
            u'category': u'active elements',
            u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.ae_default_submissions)),
        })
        data.setdefault('status', self.options.get('status', 'unlisted'))
        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        node.write_yaml(env, name, data, 'exercise')

        return [node]
Exemple #8
0
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])
        if difficulty:
            classes.append(u'difficulty-' + difficulty)

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
        }
        if 'quiz' in self.options:
            args[u'data-aplus-quiz'] = u'yes'
        if 'ajax' in self.options:
            args[u'data-aplus-ajax'] = u'yes'
        node = aplus_nodes.html(u'div', args)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get('title', '')
        else:
            data = { u'_external': True }
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            if 'lti' in self.options:
                data.update({
                    u'lti': ensure_unicode(self.options['lti']),
                    u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')),
                    u'lti_resource_link_id': ensure_unicode(self.options.get('lti_resource_link_id', u'')),
                })
                if 'lti_aplus_get_and_post' in self.options:
                    data.update({u'lti_aplus_get_and_post': True})
                if 'lti_open_in_iframe' in self.options:
                    data.update({u'lti_open_in_iframe': True})
            config_title = ''

        config_title = self.options.get('title', config_title)
        if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options:
            data[u'radar_info'] = {
                u'tokenizer': self.options.get("radar_tokenizer"),
                u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"),
            }

        category = u'submit'
        data.update({
            u'key': name,
            u'category': u'submit',
            u'scale_points': points,
            u'difficulty': difficulty or '',
            u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.program_default_submissions)),
            u'min_group_size': data.get('min_group_size', env.config.default_min_group_size),
            u'max_group_size': data.get('max_group_size', env.config.default_max_group_size),
            u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)),
            # The RST source file path is needed for fixing relative URLs
            # in the exercise description.
            '_rst_srcpath': env.doc2path(env.docname, None),
        })
        self.set_assistant_permissions(data)

        if data.get('title|i18n'):
            # Exercise config.yaml defines title|i18n for multiple languages.
            # Do not write the field "title" to data in order to avoid conflicts.
            if config_title:
                # Overwrite the title for one language since the RST directive
                # has defined the title option (or alternatively, the yaml file
                # has "title" in addition to "title|i18n", but that does not make sense).
                # env.config.language may be incorrect if the language can not be detected.
                data['title|i18n'][env.config.language] = env.config.submit_title.format(
                    key_title=key_title, config_title=config_title
                )
        else:
            formatted_title = env.config.submit_title.format(
                key_title=key_title, config_title=config_title
            )
            # If no title has been defined, use key_title as the default.
            data['title'] = formatted_title if formatted_title else key_title

        if self.content:
            self.assert_has_content()
            # Sphinx can not compile the nested RST into HTML at this stage, hence
            # the HTML instructions defined in this directive body are added to
            # the exercise YAML file only at the end of the build. Sphinx calls
            # the visit functions of the nodes in the last writing phase.
            # The instructions are added to the YAML file in the depart_html
            # function in aplus_nodes.py.
            exercise_description = aplus_nodes.html('div', {})
            exercise_description.store_html('exercise_description')
            nested_parse_with_titles(self.state, self.content, exercise_description)
            node.append(exercise_description)
            data['instructions'] = ('#!html', 'exercise_description')
        else:
            # The placeholder text is only used in the built HTML
            # (not in the YAML configurations).
            paragraph = aplus_nodes.html('p', {})
            paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder')))
            node.append(paragraph)

        data.setdefault('status', self.options.get('status', 'unlisted'))

        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        if 'category' in self.options:
            data['category'] = str(self.options['category'])

        node.write_yaml(env, name, data, 'exercise')

        return [node]
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])
        if difficulty:
            classes.append(u'difficulty-' + difficulty)

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
        }
        if 'quiz' in self.options:
            args[u'data-aplus-quiz'] = u'yes'
        if 'ajax' in self.options:
            args[u'data-aplus-ajax'] = u'yes'
        node = aplus_nodes.html(u'div', args)
        paragraph = aplus_nodes.html(u'p', {})
        paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder')))
        node.append(paragraph)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get(u'title', None)
        else:
            data = { u'_external': True }
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            if 'lti' in self.options:
                data.update({
                    u'lti': ensure_unicode(self.options['lti']),
                    u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')),
                    u'lti_resource_link_id': ensure_unicode(self.options.get('lti_resource_link_id', u'')),
                })
            config_title = None

        config_title = self.options.get('title', config_title)
        if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options:
            data[u'radar_info'] = {
                u'tokenizer': self.options.get("radar_tokenizer"),
                u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"),
            }

        category = u'submit'
        data.update({
            u'key': name,
            u'title': env.config.submit_title.format(
                key_title=key_title, config_title=config_title
            ),
            u'category': u'submit',
            u'scale_points': points,
            u'difficulty': difficulty or '',
            u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.program_default_submissions)),
            u'min_group_size': data.get('min_group_size', env.config.default_min_group_size),
            u'max_group_size': data.get('max_group_size', env.config.default_max_group_size),
            u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)),
        })
        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        if 'category' in self.options:
            data['category'] = str(self.options['category'])

        node.write_yaml(env, name, data, 'exercise')

        return [node]
Exemple #10
0
    def parse_chapter(docname, doc, parent):
        for config_file in [e.yaml_write for e in doc.traverse(aplus_nodes.html) if e.has_yaml(u'exercise')]:
            config = yaml_writer.read(config_file)
            if config.get(u'_external', False):
                exercise = config.copy()
                del exercise[u'_external']
            else:
                exercise = {
                    u'key': config[u'key'],
                    u'config': config[u'key'] + u'.yaml',
                    u'max_submissions': config.get(u'max_submissions', 0),
                    u'max_points': config.get(u'max_points', 0),
                    u'difficulty': config.get(u'difficulty', ''),
                    u'points_to_pass': config.get(u'points_to_pass', 0),
                    u'category': config[u'category'],
                    u'min_group_size': config.get(u'min_group_size', 1),
                    u'max_group_size': config.get(u'max_group_size', 1),
                    u'confirm_the_level': config.get(u'confirm_the_level', False),
                }
            allow_assistant_viewing = config.get(u'allow_assistant_viewing', app.config.allow_assistant_viewing)
            allow_assistant_grading = config.get(u'allow_assistant_grading', app.config.allow_assistant_grading)
            exercise.update({
                u'status': config.get(u'status', u'unlisted'),
                u'allow_assistant_viewing': allow_assistant_viewing,
                u'allow_assistant_grading': allow_assistant_grading,
            })
            if u'scale_points' in config:
                exercise[u'max_points'] = config.pop(u'scale_points')
            parent.append(exercise)
            if not config[u'category'] in category_keys:
                category_keys.append(config[u'category'])

        for config_file in [e.yaml_write for e in doc.traverse(aplus_nodes.html) if e.has_yaml(u'exercisecollection')]:
            config = yaml_writer.read(config_file)
            exercise = {
                u'key': config[u'key'],
                u'max_points': config.get(u'max_points', 0),
                u'points_to_pass': config.get(u'points_to_pass', 0),
                u'target_url': config[u'target_url'],
                u'target_category': config[u'target_category'],
                u'category': config[u'category'],
                u'status': config.get(u'status', u'unlisted'),
                u'title': config[u'title'],
            }
            parent.append(exercise)
            if not config[u'category'] in category_keys:
                category_keys.append(config[u'category'])


        category = u'chapter'
        for name,hidden,child in traverse_tocs(app, doc):
            meta = first_meta(child)
            status = u'hidden' if 'hidden' in meta else (
                u'unlisted' if hidden else u'ready'
            )
            chapter = {
                u'status': status,
                u'name': first_title(child),
                u'static_content': name + u'.html',
                u'category': category,
                u'use_wide_column': app.config.use_wide_column,
                u'children': [],
            }
            # If the chapter RST file is in a nested directory under the module
            # directory (e.g., module01/material/chapter.rst instead of
            # module01/chapter.rst), then the chapter key must contain parts of
            # the nested directory names in order to be unique within the module.
            # Different directories could contain files with the same names.
            key_parts = name.split('/')
            chapter['key'] = '_'.join(key_parts[1:])

            if meta:
                audience = meta.get('audience')
                if audience:
                    chapter[u'audience'] = yaml_writer.ensure_unicode(audience)
            if category in override:
                chapter.update(override[category])
            parent.append(chapter)
            if not u'chapter' in category_keys:
                category_keys.append(u'chapter')
            parse_chapter(name, child, chapter[u'children'])
    def run(self):
        key, difficulty, points = self.extract_exercise_arguments()

        env = self.state.document.settings.env
        name = u"{}_{}".format(env.docname.replace(u'/', u'_'), key)
        override = env.config.override

        classes = [u'exercise']
        if 'class' in self.options:
            classes.extend(self.options['class'])
        if difficulty:
            classes.append(u'difficulty-' + difficulty)

        # Add document nodes.
        args = {
            u'class': u' '.join(classes),
            u'data-aplus-exercise': u'yes',
        }
        if 'quiz' in self.options:
            args[u'data-aplus-quiz'] = u'yes'
        if 'ajax' in self.options:
            args[u'data-aplus-ajax'] = u'yes'
        node = aplus_nodes.html(u'div', args)

        key_title = u"{} {}".format(translations.get(env, 'exercise'), key)

        # Load or create exercise configuration.
        if 'config' in self.options:
            path = os.path.join(env.app.srcdir, self.options['config'])
            if not os.path.exists(path):
                raise SphinxError('Missing config path {}'.format(self.options['config']))
            data = yaml_writer.read(path)
            config_title = data.get(u'title', None)
        else:
            data = { u'_external': True }
            if 'url' in self.options:
                data[u'url'] = ensure_unicode(self.options['url'])
            if 'lti' in self.options:
                data.update({
                    u'lti': ensure_unicode(self.options['lti']),
                    u'lti_context_id': ensure_unicode(self.options.get('lti_context_id', u'')),
                    u'lti_resource_link_id': ensure_unicode(self.options.get('lti_resource_link_id', u'')),
                })
                if 'lti_aplus_get_and_post' in self.options:
                    data.update({u'lti_aplus_get_and_post': True})
                if 'lti_open_in_iframe' in self.options:
                    data.update({u'lti_open_in_iframe': True})
            config_title = None

        config_title = self.options.get('title', config_title)
        if "radar_tokenizer" in self.options or "radar_minimum_match_tokens" in self.options:
            data[u'radar_info'] = {
                u'tokenizer': self.options.get("radar_tokenizer"),
                u'minimum_match_tokens': self.options.get("radar_minimum_match_tokens"),
            }

        category = u'submit'
        data.update({
            u'key': name,
            u'title': env.config.submit_title.format(
                key_title=key_title, config_title=config_title
            ),
            u'category': u'submit',
            u'scale_points': points,
            u'difficulty': difficulty or '',
            u'max_submissions': self.options.get('submissions', data.get('max_submissions', env.config.program_default_submissions)),
            u'min_group_size': data.get('min_group_size', env.config.default_min_group_size),
            u'max_group_size': data.get('max_group_size', env.config.default_max_group_size),
            u'points_to_pass': self.options.get('points-to-pass', data.get('points_to_pass', 0)),
        })
        self.set_assistant_permissions(data)

        if self.content:
            self.assert_has_content()
            nested_parse_with_titles(self.state, self.content, node)
            # Sphinx can not compile the nested RST into HTML at this stage, hence
            # the HTML instructions defined in this directive body are added to
            # the exercise YAML file only at the end of the build. Sphinx calls
            # the visit functions of the nodes in the last writing phase.
            # The instructions are added to the YAML file in the depart_html
            # function in aplus_nodes.py.
        else:
            paragraph = aplus_nodes.html(u'p', {})
            paragraph.append(nodes.Text(translations.get(env, 'submit_placeholder')))
            node.append(paragraph)

        data.setdefault('status', self.options.get('status', 'unlisted'))

        if category in override:
            data.update(override[category])
            if 'url' in data:
                data['url'] = data['url'].format(key=name)

        if 'category' in self.options:
            data['category'] = str(self.options['category'])

        node.write_yaml(env, name, data, 'exercise')

        return [node]