Example #1
0
def _list_labels(handler):
    """Prepare a list of labels for use on the Assets page."""
    output = safe_dom.NodeList()
    if not handler.app_context.is_editable_fs():
        return output

    output.append(
        safe_dom.A('dashboard?action=add_label',
                   className='gcb-button'
                  ).add_text('Add Label')
        ).append(
            safe_dom.Element(
                'div', style='clear: both; padding-top: 2px;'
            )
        )
    labels = models.LabelDAO.get_all()
    if labels:
        all_labels_ul = safe_dom.Element('ul')
        output.append(all_labels_ul)
        for label_type in sorted(
            models.LabelDTO.LABEL_TYPES,
            lambda a, b: cmp(a.menu_order, b.menu_order)):

            type_li = safe_dom.Element('li').add_child(
                safe_dom.Element('strong').add_text(label_type.title))
            all_labels_ul.add_child(type_li)
            labels_of_type_ul = safe_dom.Element('ul')
            type_li.add_child(labels_of_type_ul)
            for label in sorted(
                labels, lambda a, b: cmp(a.title, b.title)):
                if label.type == label_type.type:
                    li = safe_dom.Element('li')
                    labels_of_type_ul.add_child(li)
                    li.add_text(
                        label.title
                    ).add_attribute(
                        title='id: %s, type: %s' % (label.id, label_type))
                    if label_type not in (
                        models.LabelDTO.SYSTEM_EDITABLE_LABEL_TYPES):

                        li.add_child(
                            dashboard_utils.create_edit_button(
                                'dashboard?action=edit_label&key=%s' %
                                label.id,
                                ).add_attribute(
                                    id='label_%s' % label.title))
    else:
        output.append(
            safe_dom.Element(
                'div', className='gcb-message').add_text('< none >'))
    return output
Example #2
0
def _list_labels(handler):
    """Prepare a list of labels for use on the Assets page."""
    output = safe_dom.NodeList()
    if not handler.app_context.is_editable_fs():
        return output

    output.append(
        safe_dom.A('dashboard?action=add_label',
                   className='gcb-button gcb-pull-right'
                  ).add_text('Add Label')
        ).append(
            safe_dom.Element(
                'div', style='clear: both; padding-top: 2px;'
            )
        )
    output.append(
            safe_dom.Element('h3').add_text('Labels')
    )
    labels = models.LabelDAO.get_all()
    if labels:
        all_labels_ul = safe_dom.Element('ul')
        output.append(all_labels_ul)
        for label_type in sorted(
            models.LabelDTO.LABEL_TYPES,
            lambda a, b: cmp(a.menu_order, b.menu_order)):

            type_li = safe_dom.Element('li').add_text(label_type.title)
            all_labels_ul.add_child(type_li)
            labels_of_type_ul = safe_dom.Element('ul')
            type_li.add_child(labels_of_type_ul)
            for label in sorted(
                labels, lambda a, b: cmp(a.title, b.title)):
                if label.type == label_type.type:
                    li = safe_dom.Element('li')
                    labels_of_type_ul.add_child(li)
                    li.add_text(
                        label.title
                    ).add_attribute(
                        title='id: %s, type: %s' % (label.id, label_type))
                    if label_type not in (
                        models.LabelDTO.SYSTEM_EDITABLE_LABEL_TYPES):

                        li.add_child(
                            dashboard_utils.create_edit_button(
                                'dashboard?action=edit_label&key=%s' %
                                label.id,
                                ).add_attribute(
                                    id='label_%s' % label.title))
    else:
        output.append(safe_dom.Element('blockquote').add_text('< none >'))
    return output
Example #3
0
def _render_link_outline(handler, unit):
    actions = []
    unit_data = {
        'title': unit.title,
        'class': 'link',
        'href': unit.href or '',
        'unit_id': unit.unit_id,
        'actions': actions
    }
    actions.append(_render_status_icon(handler, unit, unit.unit_id, 'unit'))
    if handler.app_context.is_editable_fs():
        url = handler.canonicalize_url(
            '/dashboard?%s') % urllib.urlencode({
                'action': 'edit_link',
                'key': unit.unit_id})
        actions.append(dashboard_utils.create_edit_button(url))
    return unit_data
Example #4
0
def _render_link_outline(handler, unit):
    actions = []
    unit_data = {
        'title': unit.title,
        'class': 'link',
        'href': unit.href or '',
        'unit_id': unit.unit_id,
        'actions': actions
    }
    actions.append(_render_status_icon(handler, unit, unit.unit_id, 'unit'))
    if handler.app_context.is_editable_fs():
        url = handler.canonicalize_url(
            '/dashboard?%s') % urllib.urlencode({
                'action': 'edit_link',
                'key': unit.unit_id})
        actions.append(dashboard_utils.create_edit_button(url))
    return unit_data
Example #5
0
def _list_and_format_file_list(
    handler, title, subfolder, tab_name,
    links=False, upload=False, prefix=None, caption_if_empty='< none >',
    edit_url_template=None, merge_local_files=False, sub_title=None,
    all_paths=None):
    """Walks files in folders and renders their names in a section."""

    # keep a list of files without merging
    unmerged_files = {}
    if merge_local_files:
        unmerged_files = dashboard_utils.list_files(
            handler, subfolder, merge_local_files=False, all_paths=all_paths)

    items = safe_dom.NodeList()
    count = 0
    for filename in dashboard_utils.list_files(
            handler, subfolder, merge_local_files=merge_local_files,
            all_paths=all_paths):
        if prefix and not filename.startswith(prefix):
            continue

        # make a <li> item
        li = safe_dom.Element('li')
        if links:
            url = urllib.quote(filename)
            li.add_child(safe_dom.Element(
                'a', href=url).add_text(filename))
        else:
            li.add_text(filename)

        # add actions if available
        if (edit_url_template and
            handler.app_context.fs.impl.is_read_write()):

            li.add_child(safe_dom.Entity('&nbsp;'))
            edit_url = edit_url_template % (
                tab_name, urllib.quote(filename), handler.request.get('action'))
            # show [overridden] + edit button if override exists
            if (filename in unmerged_files) or (not merge_local_files):
                li.add_text('[Overridden]').add_child(
                    dashboard_utils.create_edit_button(edit_url))
            # show an [override] link otherwise
            else:
                li.add_child(safe_dom.A(edit_url).add_text('[Override]'))

        count += 1
        items.append(li)

    output = safe_dom.NodeList()

    if handler.app_context.is_editable_fs() and upload:
        output.append(
            safe_dom.Element(
                'a', className='gcb-button gcb-icon-button',
                href='dashboard?%s' % urllib.urlencode(
                    {'action': 'manage_asset',
                     'from_action': handler.request.get('action'),
                     'type': tab_name,
                     'key': subfolder}),
                id='upload-button',
            ).append(
                safe_dom.Element('span', className='icon md-file-upload')
            ).append(
                safe_dom.Element('span').add_text(" Upload")
            )
        )
    if sub_title:
        output.append(safe_dom.Element(
            'div', className='gcb-message').add_text(sub_title))
    if items:
        output.append(safe_dom.Element('ol').add_children(items))
    else:
        if caption_if_empty:
            output.append(
                safe_dom.Element(
                    'div', className='gcb-message').add_text(caption_if_empty))
    return output
Example #6
0
def _list_question_groups(
    handler, all_questions, all_question_groups, locations_map):
    """Prepare a list of question groups."""
    if not handler.app_context.is_editable_fs():
        return safe_dom.NodeList()

    output = safe_dom.NodeList()
    output.append(safe_dom.Element('h3').add_text(
        'Question Groups (%s)' % len(all_question_groups)
    ))
    output.append(
        safe_dom.Element(
            'div', className='gcb-button-toolbar'
        ).append(
            safe_dom.Element(
                'a', className='gcb-button',
                href='dashboard?action=add_question_group'
            ).add_text('Add Question Group')
        )
    ).append(
        safe_dom.Element(
            'div', style='clear: both; padding-top: 2px;'
        )
    )

    # Create question groups table
    table = _add_assets_table(
        output, 'question-group-table', [
        ('Description', 25), ('Questions', 25), ('Course Locations', 25),
        ('Last Modified', 25)]
    )
    tbody = safe_dom.Element('tbody')
    table.add_child(tbody)

    if not all_question_groups:
        table.add_child(_create_empty_footer(
            'No question groups available', 4))

    quid_to_question = {long(qu.id): qu for qu in all_questions}
    for question_group in all_question_groups:
        tr = safe_dom.Element('tr', data_qgid=str(question_group.id))
        # Add description including action icons
        td = safe_dom.Element('td', className='description')
        tr.add_child(td)
        td.add_child(dashboard_utils.create_edit_button(
            'dashboard?action=edit_question_group&key=%s' % (
            question_group.id)))
        td.add_text(question_group.description)

        # Add questions
        tr.add_child(_create_list_cell([
            safe_dom.Text(descr) for descr in sorted([
                quid_to_question[long(quid)].description
                for quid in question_group.question_ids])
        ]).add_attribute(className='questions'))

        # Add locations
        tr.add_child(_create_locations_cell(
            locations_map.get(question_group.id, {})))

        # Add last modified timestamp
        tr.add_child(safe_dom.Element(
            'td',
            data_timestamp=str(question_group.last_modified),
            className='timestamp'
        ))

        tbody.add_child(tr)

    return output
Example #7
0
def _list_questions(handler, all_questions, all_question_groups, location_maps):
    """Prepare a list of the question bank contents."""
    if not handler.app_context.is_editable_fs():
        return safe_dom.NodeList()

    toolbar_template = handler.get_template(
        'question_toolbar.html', [TEMPLATE_DIR])
    toolbar_node = safe_dom.Template(toolbar_template,
        question_count=len(all_questions))

    output = safe_dom.NodeList().append(toolbar_node)

    # Create questions table
    table = _add_assets_table(
        output, 'question-table', [
        ('Description', 25), ('Question Groups', 25),
        ('Course Locations', 25), ('Last Modified', 16), ('Type', 9)]
    )
    _attach_filter_data(handler, table)
    token = crypto.XsrfTokenManager.create_xsrf_token('clone_question')
    table.add_attribute(data_clone_question_token=token)
    token = crypto.XsrfTokenManager.create_xsrf_token('add_to_question_group')
    table.add_attribute(data_qg_xsrf_token=token)
    tbody = safe_dom.Element('tbody')
    table.add_child(tbody)

    table.add_child(_create_empty_footer(
        'No questions available', 5, all_questions))

    question_to_group = {}
    for group in all_question_groups:
        for quid in group.question_ids:
            question_to_group.setdefault(long(quid), []).append(group)

    for question in all_questions:
        tr = safe_dom.Element('tr', data_quid=str(question.id))
        # Add description including action icons
        td = safe_dom.Element('td', className='description')
        tr.add_child(td)
        td.add_child(dashboard_utils.create_edit_button(
            'dashboard?action=edit_question&key=%s' % question.id))
        td.add_child(_create_preview_button())
        td.add_child(_create_clone_button(question.id))
        td.add_text(question.description)

        # Add containing question groups
        used_by_groups = question_to_group.get(question.id, [])
        cell = safe_dom.Element('td', className='groups')
        if all_question_groups:
            cell.add_child(_create_add_to_group_button())
        cell.add_child(_create_list(
            [safe_dom.Text(group.description) for group in sorted(
                used_by_groups, key=lambda g: g.description)]
        ))
        tr.add_child(cell)

        # Add locations
        locations = _get_question_locations(
            question.id, location_maps, used_by_groups)
        tr.add_child(_create_locations_cell(locations))

        # Add last modified timestamp
        tr.add_child(safe_dom.Element(
            'td',
            data_timestamp=str(question.last_modified),
            className='timestamp'
        ))

        # Add question type
        tr.add_child(safe_dom.Element('td').add_text(
            'MC' if question.type == models.QuestionDTO.MULTIPLE_CHOICE else (
                'SA' if question.type == models.QuestionDTO.SHORT_ANSWER else (
                'Unknown Type'))
        ).add_attribute(style='text-align: center'))

        # Add filter information
        filter_info = {}
        filter_info['description'] = question.description
        filter_info['type'] = question.type
        filter_info['lessons'] = []
        unit_ids = set()
        for (lesson, unit) in locations.get('lessons', ()):
            unit_ids.add(unit.unit_id)
            filter_info['lessons'].append(lesson.lesson_id)
        filter_info['units'] = list(unit_ids) + [
            a.unit_id for a in  locations.get('assessments', ())]
        filter_info['groups'] = [qg.id for qg in used_by_groups]
        filter_info['unused'] = int(not (locations and any(locations.values())))
        tr.add_attribute(data_filter=transforms.dumps(filter_info))
        tbody.add_child(tr)

    return output
Example #8
0
def _render_unit_outline(handler, course, unit):
    is_editable = handler.app_context.is_editable_fs()

    actions = []
    unit_data = {
        'title': unit.title,
        'class': 'unit',
        'href': 'unit?unit=%s' % unit.unit_id,
        'unit_id': unit.unit_id,
        'actions': actions
    }

    actions.append(_render_status_icon(handler, unit, unit.unit_id, 'unit'))
    if is_editable:
        url = handler.canonicalize_url(
            '/dashboard?%s') % urllib.urlencode({
                'action': 'edit_unit',
                'key': unit.unit_id})
        actions.append(dashboard_utils.create_edit_button(url))

    if unit.pre_assessment:
        assessment = course.find_unit_by_id(unit.pre_assessment)
        if assessment:
            assessment_outline = _render_assessment_outline(handler, assessment)
            assessment_outline['class'] = 'pre-assessment'
            unit_data['pre_assessment'] = assessment_outline

    lessons = []
    for lesson in course.get_lessons(unit.unit_id):
        actions = []
        actions.append(
            _render_status_icon(handler, lesson, lesson.lesson_id, 'lesson'))
        if is_editable:
            url = handler.get_action_url(
                'edit_lesson', key=lesson.lesson_id)
            actions.append(dashboard_utils.create_edit_button(url))

        extras = []
        for annotator in COURSE_OUTLINE_EXTRA_INFO_ANNOTATORS:
            extra_info = annotator(course, lesson)
            if extra_info:
                extras.append(extra_info)

        lessons.append({
            'title': lesson.title,
            'class': 'lesson',
            'href': 'unit?unit=%s&lesson=%s' % (
                unit.unit_id, lesson.lesson_id),
            'lesson_id': lesson.lesson_id,
            'actions': actions,
            'auto_index': lesson.auto_index,
            'extras': extras})

    unit_data['lessons'] = lessons

    if unit.post_assessment:
        assessment = course.find_unit_by_id(unit.post_assessment)
        if assessment:
            assessment_outline = _render_assessment_outline(handler, assessment)
            assessment_outline['class'] = 'post-assessment'
            unit_data['post_assessment'] = assessment_outline

    return unit_data
Example #9
0
def _render_unit_outline(handler, course, unit):
    is_editable = handler.app_context.is_editable_fs()

    actions = []
    unit_data = {
        'title': unit.title,
        'class': 'unit',
        'href': 'unit?unit=%s' % unit.unit_id,
        'unit_id': unit.unit_id,
        'actions': actions
    }

    actions.append(_render_status_icon(handler, unit, unit.unit_id, 'unit'))
    if is_editable:
        url = handler.canonicalize_url('/dashboard?%s') % urllib.urlencode(
            {
                'action': 'edit_unit',
                'key': unit.unit_id
            })
        actions.append(dashboard_utils.create_edit_button(url))

    if unit.pre_assessment:
        assessment = course.find_unit_by_id(unit.pre_assessment)
        if assessment:
            assessment_outline = _render_assessment_outline(
                handler, assessment)
            assessment_outline['class'] = 'pre-assessment'
            unit_data['pre_assessment'] = assessment_outline

    lessons = []
    for lesson in course.get_lessons(unit.unit_id):
        actions = []
        actions.append(
            _render_status_icon(handler, lesson, lesson.lesson_id, 'lesson'))
        if is_editable:
            url = handler.get_action_url('edit_lesson', key=lesson.lesson_id)
            actions.append(dashboard_utils.create_edit_button(url))

        extras = []
        for annotator in COURSE_OUTLINE_EXTRA_INFO_ANNOTATORS:
            extra_info = annotator(course, lesson)
            if extra_info:
                extras.append(extra_info)

        lessons.append({
            'title':
            lesson.title,
            'class':
            'lesson',
            'href':
            'unit?unit=%s&lesson=%s' % (unit.unit_id, lesson.lesson_id),
            'lesson_id':
            lesson.lesson_id,
            'actions':
            actions,
            'auto_index':
            lesson.auto_index,
            'extras':
            extras
        })

    unit_data['lessons'] = lessons

    if unit.post_assessment:
        assessment = course.find_unit_by_id(unit.post_assessment)
        if assessment:
            assessment_outline = _render_assessment_outline(
                handler, assessment)
            assessment_outline['class'] = 'post-assessment'
            unit_data['post_assessment'] = assessment_outline

    return unit_data
Example #10
0
def _list_question_groups(
    handler, all_questions, all_question_groups, locations_map):
    """Prepare a list of question groups."""
    if not handler.app_context.is_editable_fs():
        return safe_dom.NodeList()

    output = safe_dom.NodeList()
    output.append(
        safe_dom.Element(
            'a', className='gcb-button gcb-pull-right',
            href='dashboard?action=add_question_group'
        ).add_text('Add Question Group')
    ).append(
        safe_dom.Element(
            'div', style='clear: both; padding-top: 2px;'
        )
    )
    output.append(safe_dom.Element('h3').add_text(
        'Question Groups (%s)' % len(all_question_groups)
    ))

    # Create question groups table
    table = _add_assets_table(
        output, 'question-group-table', [
        ('Description', 25), ('Questions', 25), ('Course Locations', 25),
        ('Last Modified', 25)]
    )
    tbody = safe_dom.Element('tbody')
    table.add_child(tbody)

    if not all_question_groups:
        table.add_child(_create_empty_footer(
            'No question groups available', 4))

    quid_to_question = {long(qu.id): qu for qu in all_questions}
    for question_group in all_question_groups:
        tr = safe_dom.Element('tr', data_qgid=str(question_group.id))
        # Add description including action icons
        td = safe_dom.Element('td', className='description')
        tr.add_child(td)
        td.add_child(dashboard_utils.create_edit_button(
            'dashboard?action=edit_question_group&key=%s' % (
            question_group.id)))
        td.add_text(question_group.description)

        # Add questions
        tr.add_child(_create_list_cell([
            safe_dom.Text(descr) for descr in sorted([
                quid_to_question[long(quid)].description
                for quid in question_group.question_ids])
        ]).add_attribute(className='questions'))

        # Add locations
        tr.add_child(_create_locations_cell(
            locations_map.get(question_group.id, {})))

        # Add last modified timestamp
        tr.add_child(safe_dom.Element(
            'td',
            data_timestamp=str(question_group.last_modified),
            className='timestamp'
        ))

        tbody.add_child(tr)

    return output
Example #11
0
def _list_and_format_file_list(
    handler, title, subfolder, tab_name,
    links=False, upload=False, prefix=None, caption_if_empty='< none >',
    edit_url_template=None, merge_local_files=False, sub_title=None,
    all_paths=None):
    """Walks files in folders and renders their names in a section."""

    # keep a list of files without merging
    unmerged_files = {}
    if merge_local_files:
        unmerged_files = dashboard_utils.list_files(
            handler, subfolder, merge_local_files=False, all_paths=all_paths)

    items = safe_dom.NodeList()
    count = 0
    for filename in dashboard_utils.list_files(
            handler, subfolder, merge_local_files=merge_local_files,
            all_paths=all_paths):
        if prefix and not filename.startswith(prefix):
            continue

        # make a <li> item
        li = safe_dom.Element('li')
        if links:
            url = urllib.quote(filename)
            li.add_child(safe_dom.Element(
                'a', href=url).add_text(filename))
        else:
            li.add_text(filename)

        # add actions if available
        if (edit_url_template and
            handler.app_context.fs.impl.is_read_write()):

            li.add_child(safe_dom.Entity('&nbsp;'))
            edit_url = edit_url_template % (
                tab_name, urllib.quote(filename))
            # show [overridden] + edit button if override exists
            if (filename in unmerged_files) or (not merge_local_files):
                li.add_text('[Overridden]').add_child(
                    dashboard_utils.create_edit_button(edit_url))
            # show an [override] link otherwise
            else:
                li.add_child(safe_dom.A(edit_url).add_text('[Override]'))

        count += 1
        items.append(li)

    output = safe_dom.NodeList()

    if handler.app_context.is_editable_fs() and upload:
        output.append(
            safe_dom.Element(
                'a', className='gcb-button gcb-pull-right',
                href='dashboard?%s' % urllib.urlencode(
                    {'action': 'manage_asset',
                     'tab': tab_name,
                     'key': subfolder})
            ).add_text(
                'Upload to ' + subfolder.lstrip('/').rstrip('/'))
        ).append(
            safe_dom.Element(
                'div', style='clear: both; padding-top: 2px;'
            )
        )
    if title:
        h3 = safe_dom.Element('h3')
        if count:
            h3.add_text('%s (%s)' % (title, count))
        else:
            h3.add_text(title)
        output.append(h3)
    if sub_title:
        output.append(safe_dom.Element('blockquote').add_text(sub_title))
    if items:
        output.append(safe_dom.Element('ol').add_children(items))
    else:
        if caption_if_empty:
            output.append(
                safe_dom.Element('blockquote').add_text(caption_if_empty))
    return output
Example #12
0
def _list_questions(handler, all_questions, all_question_groups, location_maps):
    """Prepare a list of the question bank contents."""
    if not handler.app_context.is_editable_fs():
        return safe_dom.NodeList()

    output = safe_dom.NodeList().append(
        safe_dom.Element(
            'a', className='gcb-button gcb-pull-right',
            href='dashboard?action=add_mc_question'
        ).add_text('Add Multiple Choice')
    ).append(
        safe_dom.Element(
            'a', className='gcb-button gcb-pull-right',
            href='dashboard?action=add_sa_question'
        ).add_text('Add Short Answer')
    ).append(
        safe_dom.Element(
            'a', className='gcb-button gcb-pull-right',
            href='dashboard?action=import_gift_questions'
        ).add_text('Import GIFT Questions')
    ).append(_create_filter()).append(
        safe_dom.Element('div', style='clear: both; padding-top: 2px;')
    ).append(safe_dom.Element('h3').add_text(
        'Questions (%s)' % len(all_questions)
    ))

    # Create questions table
    table = _add_assets_table(
        output, 'question-table', [
        ('Description', 25), ('Question Groups', 25),
        ('Course Locations', 25), ('Last Modified', 16), ('Type', 9)]
    )
    _attach_filter_data(handler, table)
    token = crypto.XsrfTokenManager.create_xsrf_token('clone_question')
    table.add_attribute(data_clone_question_token=token)
    token = crypto.XsrfTokenManager.create_xsrf_token('add_to_question_group')
    table.add_attribute(data_qg_xsrf_token=token)
    tbody = safe_dom.Element('tbody')
    table.add_child(tbody)

    table.add_child(_create_empty_footer(
        'No questions available', 5, all_questions))

    question_to_group = {}
    for group in all_question_groups:
        for quid in group.question_ids:
            question_to_group.setdefault(long(quid), []).append(group)

    for question in all_questions:
        tr = safe_dom.Element('tr', data_quid=str(question.id))
        # Add description including action icons
        td = safe_dom.Element('td', className='description')
        tr.add_child(td)
        td.add_child(dashboard_utils.create_edit_button(
            'dashboard?action=edit_question&key=%s' % question.id))
        td.add_child(_create_preview_button())
        td.add_child(_create_clone_button(question.id))
        td.add_text(question.description)

        # Add containing question groups
        used_by_groups = question_to_group.get(question.id, [])
        cell = safe_dom.Element('td', className='groups')
        if all_question_groups:
            cell.add_child(_create_add_to_group_button())
        cell.add_child(_create_list(
            [safe_dom.Text(group.description) for group in sorted(
                used_by_groups, key=lambda g: g.description)]
        ))
        tr.add_child(cell)

        # Add locations
        locations = _get_question_locations(
            question.id, location_maps, used_by_groups)
        tr.add_child(_create_locations_cell(locations))

        # Add last modified timestamp
        tr.add_child(safe_dom.Element(
            'td',
            data_timestamp=str(question.last_modified),
            className='timestamp'
        ))

        # Add question type
        tr.add_child(safe_dom.Element('td').add_text(
            'MC' if question.type == models.QuestionDTO.MULTIPLE_CHOICE else (
                'SA' if question.type == models.QuestionDTO.SHORT_ANSWER else (
                'Unknown Type'))
        ).add_attribute(style='text-align: center'))

        # Add filter information
        filter_info = {}
        filter_info['description'] = question.description
        filter_info['type'] = question.type
        filter_info['lessons'] = []
        unit_ids = set()
        for (lesson, unit) in locations.get('lessons', ()):
            unit_ids.add(unit.unit_id)
            filter_info['lessons'].append(lesson.lesson_id)
        filter_info['units'] = list(unit_ids) + [
            a.unit_id for a in  locations.get('assessments', ())]
        filter_info['groups'] = [qg.id for qg in used_by_groups]
        filter_info['unused'] = 0 if locations else 1
        tr.add_attribute(data_filter=transforms.dumps(filter_info))
        tbody.add_child(tr)

    return output