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),
                }
            exercise.update({
                u'allow_assistant_grading': False,
                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(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):
        env = self.state.document.settings.env

        if not 'config' in self.options:
            raise SphinxError('Config option is required')
        import os
        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']))
        item_list = yaml_writer.read(path)

        itemnodes = []
        for item in item_list:
            title, info, img = [
                item.get(u"title", u""),
                item.get(u"info", u""),
                item.get(u"image_url", u"")
            ]
            _, node, data = self.create_question(
                title_text=self.arguments[0].replace(u"$title", title),
                points=False)

            more = u""
            if img:
                e = aplus_nodes.html(u"p", {u"class": u"indent"})
                e.append(
                    aplus_nodes.html(u"img", {
                        u"src": img,
                        u"alt": title,
                        u"style": u"max-height:100px;"
                    }))
                node.append(e)
                more += str(e)
            if info:
                e = aplus_nodes.html(u"p", {u"class": u"indent"})
                e.append(nodes.Text(info))
                node.append(e)
                more += str(e)

            data[u'options'] = self.generate_options(env, node)
            data[u'more'] = more
            itemnodes.append(node)

        return itemnodes
def write(app, exception):
    ''' Writes the table of contents level configuration. '''
    if exception:
        return

    course_title = app.config.course_title
    course_open = app.config.course_open_date
    course_close = app.config.course_close_date
    course_late = app.config.default_late_date
    course_penalty = app.config.default_late_penalty
    override = app.config.override

    modules = []
    category_keys = []

    def traverse_tocs(doc):
        names = []
        for toc in doc.traverse(addnodes.toctree):
            hidden = toc.attributes['hidden']
            for _, docname in toc.get('entries', []):
                names.append((docname, hidden))
        return [(name, hidden, app.env.get_doctree(name))
                for name, hidden in names]

    def first_title(doc):
        titles = doc.traverse(nodes.title)
        return titles[0].astext() if titles else u'Unnamed'

    def first_meta(doc):
        metas = doc.traverse(directives.meta.aplusmeta)
        return metas[0].options if metas else {}

    # Tries to parse date from natural text.
    def parse_date(src):
        parts = src.split(u' ', 1)
        d = parts[0]
        t = parts[1] if len(parts) > 1 else ''
        if re.match(r'^\d\d.\d\d.\d\d\d\d$', d):
            ds = d.split('.')
            d = ds[2] + u'-' + ds[1] + u'-' + ds[0]
        elif not re.match(r'^\d\d\d\d-\d\d-\d\d$', d):
            raise SphinxError(u'Invalid date ' + d)
        if not re.match(r'^\d\d(:\d\d(:\d\d)?)?$', t):
            t = u'12:00'
        return d + u' ' + t

    def parse_float(src, default):
        return float(src) if src else default

    # Recursive chapter parsing.
    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),
                }
            exercise.update({
                u'allow_assistant_grading': False,
                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(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'])

    root = app.env.get_doctree(app.config.master_doc)
    if not course_title:
        course_title = first_title(root)

    # Traverse the documents using toctree directives.
    app.info('Traverse document elements to write configuration index.')
    title_date_re = re.compile(r'.*\(DL (.+)\)')
    for docname, hidden, doc in traverse_tocs(root):
        title = first_title(doc)
        title_date_match = title_date_re.match(title)
        meta = first_meta(doc)
        status = u'hidden' if 'hidden' in meta else (
            u'unlisted' if hidden else u'ready')
        open_src = meta.get('open-time', course_open)
        close_src = meta.get(
            'close-time',
            title_date_match.group(1) if title_date_match else course_close)
        late_src = meta.get('late-time', course_late)
        module = {
            u'key': docname.split(u'/')[0],
            u'status': status,
            u'name': title,
            u'children': [],
        }
        if open_src:
            module[u'open'] = parse_date(open_src)
        if close_src:
            module[u'close'] = parse_date(close_src)
        if late_src:
            module[u'late_close'] = parse_date(late_src)
            module[u'late_penalty'] = parse_float(
                meta.get('late-penalty', course_penalty), 0.0)
        modules.append(module)
        parse_chapter(docname, doc, module[u'children'])

    # Create categories.
    category_names = app.config.category_names
    categories = {
        key: {
            u'name': category_names.get(key, key),
        }
        for key in category_keys
    }
    for key in ['chapter', 'feedback']:
        if key in categories:
            categories[key][u'status'] = u'nototal'

    # Get relative out dir.
    i = 0
    while i < len(app.outdir) and i < len(
            app.confdir) and app.outdir[i] == app.confdir[i]:
        i += 1
    outdir = app.outdir.replace("\\", "/")
    if outdir[i] == '/':
        i += 1
    outdir = outdir[i:]

    # Write the configuration index.
    config = {
        u'name': course_title,
        u'language': app.config.language,
        u'static_dir': outdir,
        u'modules': modules,
        u'categories': categories,
    }
    if course_open:
        config[u'start'] = parse_date(course_open)
    if course_close:
        config[u'end'] = parse_date(course_close)

    # Append directly configured content.
    def recursive_merge(config, append):
        if type(append) == dict:
            for key, val in append.items():
                if not key in config:
                    config[key] = val
                else:
                    recursive_merge(config[key], append[key])
        elif type(append) == list:
            for entry in append:
                add = True
                if 'key' in entry:
                    for old in config:
                        if 'key' in old and old['key'] == entry['key']:
                            recursive_merge(old, entry)
                            add = False
                if add:
                    config.append(entry)

    for path in app.config.append_content:
        recursive_merge(config, yaml_writer.read(path))

    yaml_writer.write(yaml_writer.file_path(app.env, 'index'), config)

    # Mark links to other modules.
    app.info('Retouch all files to append chapter link attributes.')
    keys = [m['key'] for m in modules]
    keys.extend(['toc', 'user', 'account'])
    for html_file in html_tools.walk(os.path.dirname(app.outdir)):
        html_tools.annotate_file_links(html_file, [u'a'], [u'href'], keys,
                                       u'data-aplus-chapter="yes" ')
    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',
        }
        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)

        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)

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

        return [node]