def make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt): flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) page_info_list = [] for group_desc in flow_desc.groups: for page_desc in group_desc.pages: points = 0 graded_count = 0 answer_count = 0 total_count = 0 visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_desc.id, page_data__page_id=page_desc.id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) answer_expected = False title = None for visit in visits: page = page_cache.get_page(group_desc.id, page_desc.id, pctx.course_commit_sha) answer_expected = answer_expected or page.expects_answer() from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) answer_feedback = visit.get_most_recent_feedback() if visit.answer is not None: answer_count += 1 total_count += 1 if (answer_feedback is not None and answer_feedback.correctness is not None): if visit.answer is None: assert answer_feedback.correctness == 0 else: points += answer_feedback.correctness graded_count += 1 if not answer_expected: continue page_info_list.append( PageAnswerStats( group_id=group_desc.id, page_id=page_desc.id, title=title, average_correctness=safe_div(points, graded_count), average_emptiness=safe_div( graded_count - answer_count, graded_count), answer_count=answer_count, total_count=total_count, url=reverse( "relate-page_analytics", args=( pctx.course_identifier, flow_id, group_desc.id, page_desc.id, )))) return page_info_list
def page_analytics(pctx, flow_id, group_id, page_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied(_("must be at least TA to view analytics")) flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) restrict_to_first_attempt = int( bool(pctx.request.GET.get("restrict_to_first_attempt") == "1")) is_multiple_submit = is_flow_multiple_submit(flow_desc) page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = (FlowPageVisit.objects .filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, )) if connection.features.can_distinct_on_fields: if restrict_to_first_attempt: visits = (visits .distinct("flow_session__participation__id") .order_by("flow_session__participation__id", "visit_time")) elif is_multiple_submit: visits = (visits .distinct("page_data__id") .order_by("page_data__id", "-visit_time")) visits = (visits .select_related("flow_session") .select_related("page_data")) normalized_answer_and_correctness_to_count = {} title = None body = None total_count = 0 graded_count = 0 for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) title = page.title(grading_page_context, visit.page_data.data) body = page.body(grading_page_context, visit.page_data.data) normalized_answer = page.normalized_answer( grading_page_context, visit.page_data.data, visit.answer) answer_feedback = visit.get_most_recent_feedback() if answer_feedback is not None: key = (normalized_answer, answer_feedback.correctness) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 graded_count += 1 else: key = (normalized_answer, None) normalized_answer_and_correctness_to_count[key] = \ normalized_answer_and_correctness_to_count.get(key, 0) + 1 total_count += 1 answer_stats = [] for (normalized_answer, correctness), count in \ normalized_answer_and_correctness_to_count.iteritems(): answer_stats.append( AnswerStats( normalized_answer=normalized_answer, correctness=correctness, count=count, percentage=safe_div(100 * count, total_count))) answer_stats = sorted( answer_stats, key=lambda astats: astats.percentage, reverse=True) return render_course_page(pctx, "course/analytics-page.html", { "flow_identifier": flow_id, "group_id": group_id, "page_id": page_id, "title": title, "body": body, "answer_stats_list": answer_stats, "restrict_to_first_attempt": restrict_to_first_attempt, })
def download_all_submissions(pctx, flow_id): if pctx.role not in [ participation_role.teaching_assistant, participation_role.instructor, participation_role.observer, ]: raise PermissionDenied( _("must be at least TA to download submissions")) from course.content import get_flow_desc flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) # {{{ find access rules tag if hasattr(flow_desc, "rules"): access_rules_tags = getattr(flow_desc.rules, "tags", []) else: access_rules_tags = [] ALL_SESSION_TAG = string_concat("<<<", _("ALL"), ">>>") # noqa session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [ (ALL_SESSION_TAG, string_concat("(", _("ALL"), ")")) ] # }}} page_ids = [ "%s/%s" % (group_desc.id, page_desc.id) for group_desc in flow_desc.groups for page_desc in group_desc.pages ] request = pctx.request if request.method == "POST": form = DownloadAllSubmissionsForm(page_ids, session_tag_choices, request.POST) if form.is_valid(): which_attempt = form.cleaned_data["which_attempt"] slash_index = form.cleaned_data["page_id"].index("/") group_id = form.cleaned_data["page_id"][:slash_index] page_id = form.cleaned_data["page_id"][slash_index + 1:] from course.utils import PageInstanceCache page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id) visits = ( FlowPageVisit.objects.filter( flow_session__course=pctx.course, flow_session__flow_id=flow_id, page_data__group_id=group_id, page_data__page_id=page_id, is_submitted_answer=True, ).select_related("flow_session").select_related( "flow_session__participation__user").select_related( "page_data") # We overwrite earlier submissions with later ones # in a dictionary below. .order_by("visit_time")) if form.cleaned_data["non_in_progress_only"]: visits = visits.filter(flow_session__in_progress=False) if form.cleaned_data["restrict_to_rules_tag"] != ALL_SESSION_TAG: visits = visits.filter(flow_session__access_rules_tag=( form.cleaned_data["restrict_to_rules_tag"])) submissions = {} for visit in visits: page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha) from course.page import PageContext grading_page_context = PageContext( course=pctx.course, repo=pctx.repo, commit_sha=pctx.course_commit_sha, flow_session=visit.flow_session) bytes_answer = page.normalized_bytes_answer( grading_page_context, visit.page_data.data, visit.answer) if which_attempt in ["first", "last"]: key = (visit.flow_session.participation.user.username, ) elif which_attempt == "all": key = (visit.flow_session.participation.user.username, str(visit.flow_session.id)) else: raise NotImplementedError() if bytes_answer is not None: if (which_attempt == "first" and key in submissions): # Already there, disregard further ones continue submissions[key] = bytes_answer from six import BytesIO from zipfile import ZipFile bio = BytesIO() with ZipFile(bio, "w") as subm_zip: for key, (extension, bytes_answer) in \ six.iteritems(submissions): subm_zip.writestr("-".join(key) + extension, bytes_answer) extra_file = request.FILES.get("extra_file") if extra_file is not None: subm_zip.writestr(extra_file.name, extra_file.read()) response = http.HttpResponse(bio.getvalue(), content_type="application/zip") response['Content-Disposition'] = ( 'attachment; filename="submissions_%s_%s_%s_%s_%s.zip"' % (pctx.course.identifier, flow_id, group_id, page_id, now().date().strftime("%Y-%m-%d"))) return response else: form = DownloadAllSubmissionsForm(page_ids, session_tag_choices) return render_course_page( pctx, "course/generic-course-form.html", { "form": form, "form_description": _("Download All Submissions in Zip file") })