Example #1
0
def get_user_queryset(request, facility, group_id):
    """Return set of users appropriate to the facility and group"""
    student_ordering = ["last_name", "first_name", "username"]
    (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(request, facility=facility)
    if group_id:
        # Narrow by group
        users = FacilityUser.objects.filter(
            group=group_id, is_teacher=False).order_by(*student_ordering)
        # Narrow all by ungroup user
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(group__isnull=True, is_teacher=False).order_by(*student_ordering)
            if facility:
                users = FacilityUser.objects.filter(facility=facility, group__isnull=True,
                                                    is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [groups_dict["groups"] for groups_dict in groups if groups_dict["facility"] == facility.id]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility), is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None), is_teacher=False).order_by(*student_ordering)

    return users
Example #2
0
def plotting_metadata_context(request,
                              facility=None,
                              topic_path=[],
                              *args,
                              **kwargs):
    """Basic context for any plot: get the data form, a dictionary of stat definitions,
    and the full gamut of facility/group objects relevant to the request."""

    # Get the form, and retrieve the API data
    form = get_data_form(request,
                         facility=facility,
                         topic_path=topic_path,
                         *args,
                         **kwargs)

    (groups, facilities,
     ungrouped_available) = get_accessible_objects_from_logged_in_user(
         request, facility=facility)

    return {
        "form": form.data,
        "stats": stats_dict,
        "groups": groups,
        "facilities": facilities,
        "ungrouped_available": ungrouped_available,
    }
Example #3
0
    def obj_get_list(self, bundle, **kwargs):
        """
        Default options will be "All" groups and if a facility has ungrouped students, also add an "Ungrouped" option.
        Then we add the list of facilities after the above default options.

        Must return no groups if there are no groups for the user.
        """

        # Call with facility=None argument to get all facilities and to determine if has ungrouped students.
        (groups, facilities,
         ungrouped_available) = get_accessible_objects_from_logged_in_user(
             bundle.request, facility=None)

        # Filter records only for all facilities accessible for the user
        qs = []
        if facilities:
            facility_ids = facilities.values_list("id", flat=True)
            # Get the facility requested by client, if there is any, and validate it
            # from the list of facilities for the user.
            facility_id = bundle.request.GET.get('facility_id')
            if facility_id and facility_id != ID_NONE:
                if facility_id in facility_ids:
                    facility = Facility.objects.get(id=facility_id)
                    ungrouped_available = facility.has_ungrouped_students
                    qs = FacilityGroup.objects.filter(facility__id=facility_id)
                else:
                    raise BadRequest(
                        "No facility found with that facility_id.")
            else:
                qs = FacilityGroup.objects.filter(
                    facility__id__in=facility_ids)

        # Flag to return only the FacilityGroup objects and not including the "All" and "Ungrouped" options.
        # TODO(cpauya): how to convert this into a kwargs above instead of a request.GET?
        groups_only = bundle.request.GET.get("groups_only", True)
        if groups_only:
            group_list = list(qs)
        else:
            default_list = []
            if qs or ungrouped_available:
                # add the "All" option
                default_list = [FacilityGroup(id=ALL_KEY, name=_("All"))]

                # add the "Ungrouped" option
                if ungrouped_available:
                    fg = FacilityGroup(id=UNGROUPED_KEY, name=_("Ungrouped"))
                    default_list.append(fg)
            # add all the facility group options for the user
            group_list = default_list + list(qs)

        # call super to trigger auth
        return super(FacilityGroupResource,
                     self).authorized_read_list(group_list, bundle)
Example #4
0
    def obj_get_list(self, bundle, **kwargs):
        """
        Default options will be "All" groups and if a facility has ungrouped students, also add an "Ungrouped" option.
        Then we add the list of facilities after the above default options.

        Must return no groups if there are no groups for the user.
        """

        # Call with facility=None argument to get all facilities and to determine if has ungrouped students.
        (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(bundle.request,
                                                                                               facility=None)

        # Filter records only for all facilities accessible for the user
        qs = []
        if facilities:
            facility_ids = facilities.values_list("id", flat=True)
            # Get the facility requested by client, if there is any, and validate it
            # from the list of facilities for the user.
            facility_id = bundle.request.GET.get('facility_id')
            if facility_id and facility_id != ID_NONE:
                if facility_id in facility_ids:
                    facility = Facility.objects.get(id=facility_id)
                    ungrouped_available = facility.has_ungrouped_students
                    qs = FacilityGroup.objects.filter(facility__id=facility_id)
                else:
                    raise BadRequest("No facility found with that facility_id.")
            else:
                qs = FacilityGroup.objects.filter(facility__id__in=facility_ids)

        # Flag to return only the FacilityGroup objects and not including the "All" and "Ungrouped" options.
        # TODO(cpauya): how to convert this into a kwargs above instead of a request.GET?
        groups_only = bundle.request.GET.get("groups_only", True)
        if groups_only:
            group_list = list(qs)
        else:
            default_list = []
            if qs or ungrouped_available:
                # add the "All" option
                default_list = [FacilityGroup(id=ALL_KEY, name=_("All"))]

                # add the "Ungrouped" option
                if ungrouped_available:
                    fg = FacilityGroup(id=UNGROUPED_KEY, name=_("Ungrouped"))
                    default_list.append(fg)
            # add all the facility group options for the user
            group_list = default_list + list(qs)

        # call super to trigger auth
        return super(FacilityGroupResource, self).authorized_read_list(group_list, bundle)
Example #5
0
def plotting_metadata_context(request, facility=None, topic_path=[], *args, **kwargs):
    """Basic context for any plot: get the data form, a dictionary of stat definitions,
    and the full gamut of facility/group objects relevant to the request."""

    # Get the form, and retrieve the API data
    form = get_data_form(request, facility=facility, topic_path=topic_path, *args, **kwargs)

    (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(request, facility=facility)

    return {
        "form": form.data,
        "stats": stats_dict,
        "groups": groups,
        "facilities": facilities,
        "ungrouped_available": ungrouped_available,
    }
Example #6
0
def get_user_queryset(request, facility, group_id):
    """Return set of users appropriate to the facility and group"""
    student_ordering = ["last_name", "first_name", "username"]
    (groups, facilities,
     ungrouped_available) = get_accessible_objects_from_logged_in_user(
         request, facility=facility)
    if group_id:
        # Narrow by group
        users = FacilityUser.objects.filter(
            group=group_id, is_teacher=False).order_by(*student_ordering)
        # Narrow all by ungroup user
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(
                group__isnull=True,
                is_teacher=False).order_by(*student_ordering)
            if facility:
                users = FacilityUser.objects.filter(
                    facility=facility, group__isnull=True,
                    is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [
            groups_dict["groups"] for groups_dict in groups
            if groups_dict["facility"] == facility.id
        ]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[
            0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility),
            is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None),
            is_teacher=False).order_by(*student_ordering)

    return users
Example #7
0
def test_view(request):
    """Test view gets data server-side and displays exam results"""
    facility, group_id, context = coach_nav_context(request, "test")
    # Get students
    users = get_user_queryset(request, facility, group_id)

    # Get the TestLog objects generated by this group of students
    # TODO(cpauya): what about queryset for ungrouped students?
    test_logs = None
    if group_id:
        test_logs = TestLog.objects.filter(user__group=group_id)
        # Narrow all by ungroup facility user
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            test_logs = TestLog.objects.filter(user__group__isnull=True)
            if facility:
                TestLog.objects.filter(user__facility=facility, user__group__isnull=True)
            else:
                TestLog.objects.filter(user__group__isnull=True)

    elif facility:
        test_logs = TestLog.objects.filter(user__facility=facility)

    else:
        # filter by all facilities and groups for the user
        (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(request, facility=facility)
        if facilities:
            facility_ids = facilities.values_list("id", flat=True)
            test_logs = TestLog.objects.filter(user__facility__id__in=facility_ids)

    # Get list of all test objects
    test_resource = TestResource()
    tests_list = test_resource._read_tests()

    # Get completed test objects (used as columns)
    completed_test_ids = set([item.test for item in test_logs])
    test_objects = [test for test in tests_list if test.test_id in completed_test_ids]

    # Create the table
    results_table = OrderedDict()
    for s in users:
        s.name = s.get_name()
        user_test_logs = [log for log in test_logs if log.user == s]
        results_table[s] = []
        for t in test_objects:
            log_object = next((log for log in user_test_logs if log.test == t.test_id), '')
            # The template expects a status and a score to display
            if log_object:
                test_object = log_object.get_test_object()
                score = round(100 * float(log_object.total_correct) / float(test_object.total_questions), 1)
                display_score = "%(score)d%% (%(correct)d/%(total_questions)d)" % {'score': score, 'correct': log_object.total_correct, 'total_questions': test_object.total_questions}
                if log_object.complete:
                    # Case: completed => we show % score
                    if score >= 80:
                        status = _("pass")
                    elif score >= 60:
                        status = _("borderline")
                    else:
                        status = _("fail" )
                    results_table[s].append({
                        "status": status,
                        "cell_display": display_score,
                        "title": status.title(),
                    })
                else:
                    # Case: has started, but has not finished => we display % score & # remaining in title
                    n_remaining = test_object.total_questions - log_object.index
                    status = _("incomplete")
                    results_table[s].append({
                        "status": status,
                        "cell_display": display_score,
                        "title": status.title() + ": " + ungettext("%(n_remaining)d problem remaining",
                                                                   n_remaining) % {'n_remaining': n_remaining},
                    })
            else:
                # Case: has not started
                status = _("not started")
                results_table[s].append({
                    "status": status,
                    "cell_display": "",
                    "title": status.title(),
                })

        # This retrieves stats for students
        score_list = [round(100 * float(result.total_correct) / float(result.get_test_object().total_questions), 1) for result in user_test_logs]
        for stat in SUMMARY_STATS:
            if score_list:
                results_table[s].append({
                    "status": "statistic",
                    "cell_display": "%d%%" % return_list_stat(score_list, stat),
                })
            else:
                results_table[s].append({
                    "status": "statistic",
                    "cell_display": "",
                })

    # This retrieves stats for tests
    stats_dict = OrderedDict()
    for stat in SUMMARY_STATS:
        stats_dict[stat] = []
        for test_obj in test_objects:
            # get the logs for this test across all users and then add summary stats
            log_scores = [round(100 * float(test_log.total_correct) / float(test_log.get_test_object().total_questions), 1) for test_log in test_logs if test_log.test == test_obj.test_id]
            stats_dict[stat].append("%d%%" % return_list_stat(log_scores, stat))

    context.update(plotting_metadata_context(request, facility=facility))
    context.update({
        "results_table": results_table,
        "test_columns": test_objects,
        "summary_stats": SUMMARY_STATS,
        "stats_dict": stats_dict,
    })

    return context
Example #8
0
def tabular_view(request, report_type="exercise"):
    """Tabular view also gets data server-side."""
    # important for setting the defaults for the coach nav bar

    language = lcode_to_django_lang(request.language)

    facility, group_id, context = coach_nav_context(request, "tabular")

    # Define how students are ordered--used to be as efficient as possible.
    student_ordering = ["last_name", "first_name", "username"]

    # Get a list of topics (sorted) and groups
    topics = [get_node_cache("Topic", language=language).get(tid["id"]) for tid in get_knowledgemap_topics(language=language) if report_type.title() in tid["contains"]]
    playlists = Playlist.all()

    (groups, facilities, ungrouped_available) = get_accessible_objects_from_logged_in_user(request, facility=facility)

    context.update(plotting_metadata_context(request, facility=facility))

    context.update({
        # For translators: the following two translations are nouns
        "report_types": ({"value": "exercise", "name":_("exercise")}, {"value": "video", "name": _("video")}),
        "request_report_type": report_type,
        "topics": [{"id": t["id"], "title": t["title"]} for t in topics if t],
        "playlists": [{"id": p.id, "title": p.title, "tag": p.tag} for p in playlists if p],
    })

    # get querystring info
    topic_id = request.GET.get("topic", "")
    playlist_id = request.GET.get("playlist", "")
    # No valid data; just show generic
    # Exactly one of topic_id or playlist_id should be present
    if not ((topic_id or playlist_id) and not (topic_id and playlist_id)):
        if playlists:
            messages.add_message(request, WARNING, _("Please select a playlist."))
        elif topics:
            messages.add_message(request, WARNING, _("Please select a topic."))
        return context

    playlist = (filter(lambda p: p.id == playlist_id, Playlist.all()) or [None])[0]

    if group_id:
        # Narrow by group
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(group__isnull=True, is_teacher=False)
            if facility:
                # filter only those ungrouped students for the facility
                users = users.filter(facility=facility)
                users = users.order_by(*student_ordering)
            else:
                # filter all ungroup students
                users = FacilityUser.objects.filter(group__isnull=True, is_teacher=False).order_by(*student_ordering)

        else:
            users = FacilityUser.objects.filter(
                group=group_id, is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [groups_dict["groups"] for groups_dict in groups if groups_dict["facility"] == facility.id]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility), is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None), is_teacher=False).order_by(*student_ordering)

    # We have enough data to render over a group of students
    # Get type-specific information
    if report_type == "exercise":
        # Fill in exercises
        if topic_id:
            exercises = get_topic_exercises(topic_id=topic_id)
        elif playlist:
            exercises = playlist.get_playlist_entries("Exercise", language=language)

        context["exercises"] = exercises

        # More code, but much faster
        exercise_names = [ex["id"] for ex in context["exercises"]]
        # Get students
        context["students"] = []
        exlogs = ExerciseLog.objects \
            .filter(user__in=users, exercise_id__in=exercise_names) \
            .order_by(*["user__%s" % field for field in student_ordering]) \
            .values("user__id", "struggling", "complete", "exercise_id")
        exlogs = list(exlogs)  # force the query to be evaluated

        exlog_idx = 0
        for user in users:
            log_table = {}
            while exlog_idx < len(exlogs) and exlogs[exlog_idx]["user__id"] == user.id:
                log_table[exlogs[exlog_idx]["exercise_id"]] = exlogs[exlog_idx]
                exlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "exercise_logs": log_table,
            })

    elif report_type == "video":
        # Fill in videos
        if topic_id:
            context["videos"] = get_topic_videos(topic_id=topic_id)
        elif playlist:
            context["videos"] = playlist.get_playlist_entries("Video", language=language)

        # More code, but much faster
        video_ids = [vid["id"] for vid in context["videos"]]
        # Get students
        context["students"] = []
        vidlogs = VideoLog.objects \
            .filter(user__in=users, video_id__in=video_ids) \
            .order_by(*["user__%s" % field for field in student_ordering])\
            .values("user__id", "complete", "video_id", "total_seconds_watched", "points")
        vidlogs = list(vidlogs)  # force the query to be executed now

        vidlog_idx = 0
        for user in users:
            log_table = {}
            while vidlog_idx < len(vidlogs) and vidlogs[vidlog_idx]["user__id"] == user.id:
                log_table[vidlogs[vidlog_idx]["video_id"]] = vidlogs[vidlog_idx]
                vidlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "video_logs": log_table,
            })
    else:
        raise Http404(_("Unknown report_type: %(report_type)s") % {"report_type": report_type})

    # Validate results by showing user messages.
    if not users:
        # 1. check group facility groups
        if len(groups) > 0 and not groups[0]['groups']:
            # 1. No groups available (for facility) and "no students" returned.
            messages.add_message(request, WARNING,
                                 _("No learner accounts have been created for selected facility/group."))
        elif topic_id and playlist_id:
            # 2. Both topic and playlist are selected.
            messages.add_message(request, WARNING, _("Please select either a topic or a playlist above, but not both."))
        elif not topic_id and not playlist_id:
            # 3. Group was selected, but data not queried because a topic or playlist was not selected.
            if playlists:
                # 4. No playlist was selected.
                messages.add_message(request, WARNING, _("Please select a playlist."))
            elif topics:
                # 5. No topic was selected.
                messages.add_message(request, WARNING, _("Please select a topic."))
        else:
            # 6. Everything specified, but no users fit the query.
            messages.add_message(request, WARNING, _("No learner accounts in this group have been created."))
    # End: Validate results by showing user messages.

    log_coach_report_view(request)

    return context
Example #9
0
def test_view(request):
    """Test view gets data server-side and displays exam results"""
    facility, group_id, context = coach_nav_context(request, "test")
    # Get students
    users = get_user_queryset(request, facility, group_id)

    # Get the TestLog objects generated by this group of students
    # TODO(cpauya): what about queryset for ungrouped students?
    test_logs = None
    if group_id:
        test_logs = TestLog.objects.filter(user__group=group_id)
        # Narrow all by ungroup facility user
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            test_logs = TestLog.objects.filter(user__group__isnull=True)
            if facility:
                TestLog.objects.filter(user__facility=facility,
                                       user__group__isnull=True)
            else:
                TestLog.objects.filter(user__group__isnull=True)

    elif facility:
        test_logs = TestLog.objects.filter(user__facility=facility)

    else:
        # filter by all facilities and groups for the user
        (groups, facilities,
         ungrouped_available) = get_accessible_objects_from_logged_in_user(
             request, facility=facility)
        if facilities:
            facility_ids = facilities.values_list("id", flat=True)
            test_logs = TestLog.objects.filter(
                user__facility__id__in=facility_ids)

    # Get list of all test objects
    test_resource = TestResource()
    tests_list = test_resource._read_tests()

    # Get completed test objects (used as columns)
    completed_test_ids = set([item.test for item in test_logs])
    test_objects = [
        test for test in tests_list if test.test_id in completed_test_ids
    ]

    # Create the table
    results_table = OrderedDict()
    for s in users:
        s.name = s.get_name()
        user_test_logs = [log for log in test_logs if log.user == s]
        results_table[s] = []
        for t in test_objects:
            log_object = next(
                (log for log in user_test_logs if log.test == t.test_id), '')
            # The template expects a status and a score to display
            if log_object:
                test_object = log_object.get_test_object()
                score = round(
                    100 * float(log_object.total_correct) /
                    float(test_object.total_questions), 1)
                display_score = "%(score)d%% (%(correct)d/%(total_questions)d)" % {
                    'score': score,
                    'correct': log_object.total_correct,
                    'total_questions': test_object.total_questions
                }
                if log_object.complete:
                    # Case: completed => we show % score
                    if score >= 80:
                        status = _("pass")
                    elif score >= 60:
                        status = _("borderline")
                    else:
                        status = _("fail")
                    results_table[s].append({
                        "status": status,
                        "cell_display": display_score,
                        "title": status.title(),
                    })
                else:
                    # Case: has started, but has not finished => we display % score & # remaining in title
                    n_remaining = test_object.total_questions - log_object.index
                    status = _("incomplete")
                    results_table[s].append({
                        "status": status,
                        "cell_display": display_score,
                        "title": status.title() + ": " +
                        ungettext("%(n_remaining)d problem remaining",
                                  n_remaining) % {
                                      'n_remaining': n_remaining
                                  },
                    })
            else:
                # Case: has not started
                status = _("not started")
                results_table[s].append({
                    "status": status,
                    "cell_display": "",
                    "title": status.title(),
                })

        # This retrieves stats for students
        score_list = [
            round(
                100 * float(result.total_correct) /
                float(result.get_test_object().total_questions), 1)
            for result in user_test_logs
        ]
        for stat in SUMMARY_STATS:
            if score_list:
                results_table[s].append({
                    "status":
                    "statistic",
                    "cell_display":
                    "%d%%" % return_list_stat(score_list, stat),
                })
            else:
                results_table[s].append({
                    "status": "statistic",
                    "cell_display": "",
                })

    # This retrieves stats for tests
    stats_dict = OrderedDict()
    for stat in SUMMARY_STATS:
        stats_dict[stat] = []
        for test_obj in test_objects:
            # get the logs for this test across all users and then add summary stats
            log_scores = [
                round(
                    100 * float(test_log.total_correct) /
                    float(test_log.get_test_object().total_questions), 1)
                for test_log in test_logs if test_log.test == test_obj.test_id
            ]
            stats_dict[stat].append("%d%%" %
                                    return_list_stat(log_scores, stat))

    context.update(plotting_metadata_context(request, facility=facility))
    context.update({
        "results_table": results_table,
        "test_columns": test_objects,
        "summary_stats": SUMMARY_STATS,
        "stats_dict": stats_dict,
    })

    return context
Example #10
0
def tabular_view(request, report_type="exercise"):
    """Tabular view also gets data server-side."""
    # important for setting the defaults for the coach nav bar

    language = lcode_to_django_lang(request.language)

    facility, group_id, context = coach_nav_context(request, "tabular")

    # Define how students are ordered--used to be as efficient as possible.
    student_ordering = ["last_name", "first_name", "username"]

    # Get a list of topics (sorted) and groups
    topics = [
        get_node_cache("Topic", language=language).get(tid["id"])
        for tid in get_knowledgemap_topics(language=language)
        if report_type.title() in tid["contains"]
    ]
    playlists = Playlist.all()

    (groups, facilities,
     ungrouped_available) = get_accessible_objects_from_logged_in_user(
         request, facility=facility)

    context.update(plotting_metadata_context(request, facility=facility))

    context.update({
        # For translators: the following two translations are nouns
        "report_types": ({
            "value": "exercise",
            "name": _("exercise")
        }, {
            "value": "video",
            "name": _("video")
        }),
        "request_report_type":
        report_type,
        "topics": [{
            "id": t["id"],
            "title": t["title"]
        } for t in topics if t],
        "playlists": [{
            "id": p.id,
            "title": p.title,
            "tag": p.tag
        } for p in playlists if p],
    })

    # get querystring info
    topic_id = request.GET.get("topic", "")
    playlist_id = request.GET.get("playlist", "")
    # No valid data; just show generic
    # Exactly one of topic_id or playlist_id should be present
    if not ((topic_id or playlist_id) and not (topic_id and playlist_id)):
        if playlists:
            messages.add_message(request, WARNING,
                                 _("Please select a playlist."))
        elif topics:
            messages.add_message(request, WARNING, _("Please select a topic."))
        return context

    playlist = (filter(lambda p: p.id == playlist_id, Playlist.all())
                or [None])[0]

    if group_id:
        # Narrow by group
        if group_id == control_panel_api_resources.UNGROUPED_KEY:
            users = FacilityUser.objects.filter(group__isnull=True,
                                                is_teacher=False)
            if facility:
                # filter only those ungrouped students for the facility
                users = users.filter(facility=facility)
                users = users.order_by(*student_ordering)
            else:
                # filter all ungroup students
                users = FacilityUser.objects.filter(
                    group__isnull=True,
                    is_teacher=False).order_by(*student_ordering)

        else:
            users = FacilityUser.objects.filter(
                group=group_id, is_teacher=False).order_by(*student_ordering)

    elif facility:
        # Narrow by facility
        search_groups = [
            groups_dict["groups"] for groups_dict in groups
            if groups_dict["facility"] == facility.id
        ]
        assert len(search_groups) <= 1, "Should only have one or zero matches."

        # Return groups and ungrouped
        search_groups = search_groups[
            0]  # make sure to include ungrouped students
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None, facility=facility),
            is_teacher=False).order_by(*student_ordering)

    else:
        # Show all (including ungrouped)
        search_groups = []
        for groups_dict in groups:
            search_groups += groups_dict["groups"]
        users = FacilityUser.objects.filter(
            Q(group__in=search_groups) | Q(group=None),
            is_teacher=False).order_by(*student_ordering)

    # We have enough data to render over a group of students
    # Get type-specific information
    if report_type == "exercise":
        # Fill in exercises
        if topic_id:
            exercises = get_topic_exercises(topic_id=topic_id)
        elif playlist:
            exercises = playlist.get_playlist_entries("Exercise",
                                                      language=language)

        context["exercises"] = exercises

        # More code, but much faster
        exercise_names = [ex["id"] for ex in context["exercises"]]
        # Get students
        context["students"] = []
        exlogs = ExerciseLog.objects \
            .filter(user__in=users, exercise_id__in=exercise_names) \
            .order_by(*["user__%s" % field for field in student_ordering]) \
            .values("user__id", "struggling", "complete", "exercise_id")
        exlogs = list(exlogs)  # force the query to be evaluated

        exlog_idx = 0
        for user in users:
            log_table = {}
            while exlog_idx < len(
                    exlogs) and exlogs[exlog_idx]["user__id"] == user.id:
                log_table[exlogs[exlog_idx]["exercise_id"]] = exlogs[exlog_idx]
                exlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "exercise_logs": log_table,
            })

    elif report_type == "video":
        # Fill in videos
        if topic_id:
            context["videos"] = get_topic_videos(topic_id=topic_id)
        elif playlist:
            context["videos"] = playlist.get_playlist_entries(
                "Video", language=language)

        # More code, but much faster
        video_ids = [vid["id"] for vid in context["videos"]]
        # Get students
        context["students"] = []
        vidlogs = VideoLog.objects \
            .filter(user__in=users, video_id__in=video_ids) \
            .order_by(*["user__%s" % field for field in student_ordering])\
            .values("user__id", "complete", "video_id", "total_seconds_watched", "points")
        vidlogs = list(vidlogs)  # force the query to be executed now

        vidlog_idx = 0
        for user in users:
            log_table = {}
            while vidlog_idx < len(
                    vidlogs) and vidlogs[vidlog_idx]["user__id"] == user.id:
                log_table[vidlogs[vidlog_idx]
                          ["video_id"]] = vidlogs[vidlog_idx]
                vidlog_idx += 1

            context["students"].append({  # this could be DRYer
                "first_name": user.first_name,
                "last_name": user.last_name,
                "username": user.username,
                "name": user.get_name(),
                "id": user.id,
                "video_logs": log_table,
            })
    else:
        raise Http404(
            _("Unknown report_type: %(report_type)s") %
            {"report_type": report_type})

    # Validate results by showing user messages.
    if not users:
        # 1. check group facility groups
        if len(groups) > 0 and not groups[0]['groups']:
            # 1. No groups available (for facility) and "no students" returned.
            messages.add_message(
                request, WARNING,
                _("No learner accounts have been created for selected facility/group."
                  ))
        elif topic_id and playlist_id:
            # 2. Both topic and playlist are selected.
            messages.add_message(
                request, WARNING,
                _("Please select either a topic or a playlist above, but not both."
                  ))
        elif not topic_id and not playlist_id:
            # 3. Group was selected, but data not queried because a topic or playlist was not selected.
            if playlists:
                # 4. No playlist was selected.
                messages.add_message(request, WARNING,
                                     _("Please select a playlist."))
            elif topics:
                # 5. No topic was selected.
                messages.add_message(request, WARNING,
                                     _("Please select a topic."))
        else:
            # 6. Everything specified, but no users fit the query.
            messages.add_message(
                request, WARNING,
                _("No learner accounts in this group have been created."))
    # End: Validate results by showing user messages.

    log_coach_report_view(request)

    return context