Merge pull request #1655 from edx/adam/more-granular-grading-transactions
Adam/more granular grading transactions
This commit is contained in:
@@ -4,9 +4,11 @@ from __future__ import division
|
||||
import random
|
||||
import logging
|
||||
|
||||
from contextlib import contextmanager
|
||||
from collections import defaultdict
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import transaction
|
||||
|
||||
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
|
||||
from xblock.fields import Scope
|
||||
@@ -121,8 +123,20 @@ def answer_distributions(request, course):
|
||||
return counts
|
||||
|
||||
|
||||
def grade(student, request, course, field_data_cache=None, keep_raw_scores=False):
|
||||
@transaction.commit_manually
|
||||
def grade(student, request, course, keep_raw_scores=False):
|
||||
"""
|
||||
Wraps "_grade" with the manual_transaction context manager just in case
|
||||
there are unanticipated errors.
|
||||
"""
|
||||
with manual_transaction():
|
||||
return _grade(student, request, course, keep_raw_scores)
|
||||
|
||||
|
||||
def _grade(student, request, course, keep_raw_scores):
|
||||
"""
|
||||
Unwrapped version of "grade"
|
||||
|
||||
This grades a student as quickly as possible. It returns the
|
||||
output from the course grader, augmented with the final letter
|
||||
grade. The keys in the output are:
|
||||
@@ -132,20 +146,17 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False
|
||||
- grade : A final letter grade.
|
||||
- percent : The final percent for the class (rounded up).
|
||||
- section_breakdown : A breakdown of each section that makes
|
||||
up the grade. (For display)
|
||||
up the grade. (For display)
|
||||
- grade_breakdown : A breakdown of the major components that
|
||||
make up the final grade. (For display)
|
||||
- keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module
|
||||
make up the final grade. (For display)
|
||||
- keep_raw_scores : if True, then value for key 'raw_scores' contains scores
|
||||
for every graded module
|
||||
|
||||
More information on the format is in the docstring for CourseGrader.
|
||||
"""
|
||||
|
||||
grading_context = course.grading_context
|
||||
raw_scores = []
|
||||
|
||||
if field_data_cache is None:
|
||||
field_data_cache = FieldDataCache(grading_context['all_descriptors'], course.id, student)
|
||||
|
||||
totaled_scores = {}
|
||||
# This next complicated loop is just to collect the totaled_scores, which is
|
||||
# passed to the grader
|
||||
@@ -155,26 +166,22 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False
|
||||
section_descriptor = section['section_descriptor']
|
||||
section_name = section_descriptor.display_name_with_default
|
||||
|
||||
should_grade_section = False
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.)
|
||||
should_grade_section = any(
|
||||
descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors']
|
||||
)
|
||||
|
||||
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
|
||||
for moduledescriptor in section['xmoduledescriptors']:
|
||||
# some problems have state that is updated independently of interaction
|
||||
# with the LMS, so they need to always be scored. (E.g. foldit.)
|
||||
if moduledescriptor.always_recalculate_grades:
|
||||
should_grade_section = True
|
||||
break
|
||||
|
||||
# Create a fake key to pull out a StudentModule object from the FieldDataCache
|
||||
|
||||
key = DjangoKeyValueStore.Key(
|
||||
Scope.user_state,
|
||||
student.id,
|
||||
moduledescriptor.location,
|
||||
None
|
||||
)
|
||||
if field_data_cache.find(key):
|
||||
should_grade_section = True
|
||||
break
|
||||
if not should_grade_section:
|
||||
with manual_transaction():
|
||||
should_grade_section = StudentModule.objects.filter(
|
||||
student=student,
|
||||
module_type='problem',
|
||||
module_state_key__in=[
|
||||
descriptor.location for descriptor in section['xmoduledescriptors']
|
||||
]
|
||||
).exists()
|
||||
|
||||
if should_grade_section:
|
||||
scores = []
|
||||
@@ -183,11 +190,13 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False
|
||||
'''creates an XModule instance given a descriptor'''
|
||||
# TODO: We need the request to pass into here. If we could forego that, our arguments
|
||||
# would be simpler
|
||||
with manual_transaction():
|
||||
field_data_cache = FieldDataCache([descriptor], course.id, student)
|
||||
return get_module_for_descriptor(student, request, descriptor, field_data_cache, course.id)
|
||||
|
||||
for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module):
|
||||
|
||||
(correct, total) = get_score(course.id, student, module_descriptor, create_module, field_data_cache)
|
||||
(correct, total) = get_score(course.id, student, module_descriptor, create_module)
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
@@ -256,11 +265,23 @@ def grade_for_percentage(grade_cutoffs, percentage):
|
||||
return letter_grade
|
||||
|
||||
|
||||
@transaction.commit_manually
|
||||
def progress_summary(student, request, course):
|
||||
"""
|
||||
Wraps "_progress_summary" with the manual_transaction context manager just
|
||||
in case there are unanticipated errors.
|
||||
"""
|
||||
with manual_transaction():
|
||||
return _progress_summary(student, request, course)
|
||||
|
||||
|
||||
# TODO: This method is not very good. It was written in the old course style and
|
||||
# then converted over and performance is not good. Once the progress page is redesigned
|
||||
# to not have the progress summary this method should be deleted (so it won't be copied).
|
||||
def progress_summary(student, request, course, field_data_cache):
|
||||
def _progress_summary(student, request, course):
|
||||
"""
|
||||
Unwrapped version of "progress_summary".
|
||||
|
||||
This pulls a summary of all problems in the course.
|
||||
|
||||
Returns
|
||||
@@ -273,20 +294,21 @@ def progress_summary(student, request, course, field_data_cache):
|
||||
Arguments:
|
||||
student: A User object for the student to grade
|
||||
course: A Descriptor containing the course to grade
|
||||
field_data_cache: A FieldDataCache initialized with all
|
||||
instance_modules for the student
|
||||
|
||||
If the student does not have access to load the course module, this function
|
||||
will return None.
|
||||
|
||||
"""
|
||||
|
||||
# TODO: We need the request to pass into here. If we could forego that, our arguments
|
||||
# would be simpler
|
||||
course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id)
|
||||
if not course_module:
|
||||
# This student must not have access to the course.
|
||||
return None
|
||||
with manual_transaction():
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course.id, student, course, depth=None
|
||||
)
|
||||
# TODO: We need the request to pass into here. If we could
|
||||
# forego that, our arguments would be simpler
|
||||
course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id)
|
||||
if not course_module:
|
||||
# This student must not have access to the course.
|
||||
return None
|
||||
|
||||
chapters = []
|
||||
# Don't include chapters that aren't displayable (e.g. due to error)
|
||||
@@ -296,50 +318,52 @@ def progress_summary(student, request, course, field_data_cache):
|
||||
continue
|
||||
|
||||
sections = []
|
||||
|
||||
for section_module in chapter_module.get_display_items():
|
||||
# Skip if the section is hidden
|
||||
if section_module.hide_from_toc:
|
||||
continue
|
||||
|
||||
# Same for sections
|
||||
graded = section_module.graded
|
||||
scores = []
|
||||
|
||||
module_creator = section_module.xmodule_runtime.get_module
|
||||
|
||||
for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator):
|
||||
|
||||
course_id = course.id
|
||||
(correct, total) = get_score(course_id, student, module_descriptor, module_creator, field_data_cache)
|
||||
if correct is None and total is None:
|
||||
with manual_transaction():
|
||||
if section_module.hide_from_toc:
|
||||
continue
|
||||
|
||||
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
|
||||
graded = section_module.graded
|
||||
scores = []
|
||||
|
||||
scores.reverse()
|
||||
section_total, _ = graders.aggregate_scores(
|
||||
scores, section_module.display_name_with_default)
|
||||
module_creator = section_module.xmodule_runtime.get_module
|
||||
|
||||
module_format = section_module.format if section_module.format is not None else ''
|
||||
sections.append({
|
||||
'display_name': section_module.display_name_with_default,
|
||||
'url_name': section_module.url_name,
|
||||
'scores': scores,
|
||||
'section_total': section_total,
|
||||
'format': module_format,
|
||||
'due': section_module.due,
|
||||
'graded': graded,
|
||||
})
|
||||
for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator):
|
||||
|
||||
chapters.append({'course': course.display_name_with_default,
|
||||
'display_name': chapter_module.display_name_with_default,
|
||||
'url_name': chapter_module.url_name,
|
||||
'sections': sections})
|
||||
course_id = course.id
|
||||
(correct, total) = get_score(course_id, student, module_descriptor, module_creator)
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
|
||||
|
||||
scores.reverse()
|
||||
section_total, _ = graders.aggregate_scores(
|
||||
scores, section_module.display_name_with_default)
|
||||
|
||||
module_format = section_module.format if section_module.format is not None else ''
|
||||
sections.append({
|
||||
'display_name': section_module.display_name_with_default,
|
||||
'url_name': section_module.url_name,
|
||||
'scores': scores,
|
||||
'section_total': section_total,
|
||||
'format': module_format,
|
||||
'due': section_module.due,
|
||||
'graded': graded,
|
||||
})
|
||||
|
||||
chapters.append({
|
||||
'course': course.display_name_with_default,
|
||||
'display_name': chapter_module.display_name_with_default,
|
||||
'url_name': chapter_module.url_name,
|
||||
'sections': sections
|
||||
})
|
||||
|
||||
return chapters
|
||||
|
||||
|
||||
def get_score(course_id, user, problem_descriptor, module_creator, field_data_cache):
|
||||
def get_score(course_id, user, problem_descriptor, module_creator):
|
||||
"""
|
||||
Return the score for a user on a problem, as a tuple (correct, total).
|
||||
e.g. (5,7) if you got 5 out of 7 points.
|
||||
@@ -372,15 +396,15 @@ def get_score(course_id, user, problem_descriptor, module_creator, field_data_ca
|
||||
# These are not problems, and do not have a score
|
||||
return (None, None)
|
||||
|
||||
# Create a fake KeyValueStore key to pull out the StudentModule
|
||||
key = DjangoKeyValueStore.Key(
|
||||
Scope.user_state,
|
||||
user.id,
|
||||
problem_descriptor.location,
|
||||
None
|
||||
)
|
||||
|
||||
student_module = field_data_cache.find(key)
|
||||
try:
|
||||
student_module = StudentModule.objects.get(
|
||||
student=user,
|
||||
course_id=course_id,
|
||||
module_type='problem',
|
||||
module_state_key=problem_descriptor.location
|
||||
)
|
||||
except StudentModule.DoesNotExist:
|
||||
student_module = None
|
||||
|
||||
if student_module is not None and student_module.max_grade is not None:
|
||||
correct = student_module.grade if student_module.grade is not None else 0
|
||||
@@ -411,3 +435,16 @@ def get_score(course_id, user, problem_descriptor, module_creator, field_data_ca
|
||||
total = weight
|
||||
|
||||
return (correct, total)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def manual_transaction():
|
||||
"""A context manager for managing manual transactions"""
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
transaction.rollback()
|
||||
log.exception('Due to an error, this transaction has been rolled back')
|
||||
raise
|
||||
else:
|
||||
transaction.commit()
|
||||
|
||||
@@ -236,14 +236,11 @@ class TestCourseGrader(TestSubmittingProblems):
|
||||
make up the final grade. (For display)
|
||||
"""
|
||||
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
self.course.id, self.student_user, self.course)
|
||||
fake_request = self.factory.get(
|
||||
reverse('progress', kwargs={'course_id': self.course.id})
|
||||
)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.course.id}))
|
||||
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.course, field_data_cache)
|
||||
return grades.grade(self.student_user, fake_request, self.course)
|
||||
|
||||
def get_progress_summary(self):
|
||||
"""
|
||||
@@ -257,16 +254,13 @@ class TestCourseGrader(TestSubmittingProblems):
|
||||
etc.
|
||||
"""
|
||||
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
self.course.id, self.student_user, self.course)
|
||||
fake_request = self.factory.get(
|
||||
reverse('progress', kwargs={'course_id': self.course.id})
|
||||
)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user,
|
||||
fake_request,
|
||||
self.course,
|
||||
field_data_cache)
|
||||
progress_summary = grades.progress_summary(
|
||||
self.student_user, fake_request, self.course
|
||||
)
|
||||
return progress_summary
|
||||
|
||||
def check_grade_percent(self, percent):
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.db import transaction
|
||||
from markupsafe import escape
|
||||
|
||||
from courseware import grades
|
||||
@@ -643,8 +644,9 @@ def mktg_course_about(request, course_id):
|
||||
except (ValueError, Http404) as e:
|
||||
# if a course does not exist yet, display a coming
|
||||
# soon button
|
||||
return render_to_response('courseware/mktg_coming_soon.html',
|
||||
{'course_id': course_id})
|
||||
return render_to_response(
|
||||
'courseware/mktg_coming_soon.html', {'course_id': course_id}
|
||||
)
|
||||
|
||||
registered = registered_for_course(course, request.user)
|
||||
|
||||
@@ -659,21 +661,36 @@ def mktg_course_about(request, course_id):
|
||||
settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'))
|
||||
course_modes = CourseMode.modes_for_course(course.id)
|
||||
|
||||
return render_to_response('courseware/mktg_course_about.html',
|
||||
{
|
||||
'course': course,
|
||||
'registered': registered,
|
||||
'allow_registration': allow_registration,
|
||||
'course_target': course_target,
|
||||
'show_courseware_link': show_courseware_link,
|
||||
'course_modes': course_modes,
|
||||
})
|
||||
return render_to_response(
|
||||
'courseware/mktg_course_about.html',
|
||||
{
|
||||
'course': course,
|
||||
'registered': registered,
|
||||
'allow_registration': allow_registration,
|
||||
'course_target': course_target,
|
||||
'show_courseware_link': show_courseware_link,
|
||||
'course_modes': course_modes,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@transaction.commit_manually
|
||||
def progress(request, course_id, student_id=None):
|
||||
""" User progress. We show the grade bar and every problem score.
|
||||
"""
|
||||
Wraps "_progress" with the manual_transaction context manager just in case
|
||||
there are unanticipated errors.
|
||||
"""
|
||||
with grades.manual_transaction():
|
||||
return _progress(request, course_id, student_id)
|
||||
|
||||
|
||||
def _progress(request, course_id, student_id):
|
||||
"""
|
||||
Unwrapped version of "progress".
|
||||
|
||||
User progress. We show the grade bar and every problem score.
|
||||
|
||||
Course staff are allowed to see the progress of students in their class.
|
||||
"""
|
||||
@@ -689,33 +706,33 @@ def progress(request, course_id, student_id=None):
|
||||
raise Http404
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
# NOTE: To make sure impersonation by instructor works, use
|
||||
# student instead of request.user in the rest of the function.
|
||||
# NOTE: To make sure impersonation by instructor works, use
|
||||
# student instead of request.user in the rest of the function.
|
||||
|
||||
# The pre-fetching of groups is done to make auth checks not require an
|
||||
# additional DB lookup (this kills the Progress page in particular).
|
||||
# The pre-fetching of groups is done to make auth checks not require an
|
||||
# additional DB lookup (this kills the Progress page in particular).
|
||||
student = User.objects.prefetch_related("groups").get(id=student.id)
|
||||
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_id, student, course, depth=None)
|
||||
courseware_summary = grades.progress_summary(student, request, course)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, request, course,
|
||||
field_data_cache)
|
||||
grade_summary = grades.grade(student, request, course, field_data_cache)
|
||||
grade_summary = grades.grade(student, request, course)
|
||||
|
||||
if courseware_summary is None:
|
||||
#This means the student didn't have access to the course (which the instructor requested)
|
||||
raise Http404
|
||||
|
||||
context = {'course': course,
|
||||
'courseware_summary': courseware_summary,
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
'student': student,
|
||||
}
|
||||
context.update()
|
||||
context = {
|
||||
'course': course,
|
||||
'courseware_summary': courseware_summary,
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
'student': student,
|
||||
}
|
||||
|
||||
return render_to_response('courseware/progress.html', context)
|
||||
with grades.manual_transaction():
|
||||
response = render_to_response('courseware/progress.html', context)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user