Files
edx-platform/lms/djangoapps/grades/management/commands/compute_grades.py
2021-02-22 12:58:41 +05:00

149 lines
5.3 KiB
Python

"""
Command to compute all grades for specified courses.
"""
import hashlib
import logging
from django.core.management.base import BaseCommand
from lms.djangoapps.grades import tasks
from lms.djangoapps.grades.config.models import ComputeGradesSetting
from openedx.core.lib.command_utils import get_mutually_exclusive_required_option, parse_course_keys
from xmodule.modulestore.django import modulestore
log = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Example usage:
$ ./manage.py lms compute_grades --all_courses --settings=devstack
$ ./manage.py lms compute_grades 'edX/DemoX/Demo_Course' --settings=devstack
"""
args = '<course_id course_id ...>'
help = 'Computes grade values for all learners in specified courses.'
def add_arguments(self, parser):
"""
Entry point for subclassed commands to add custom arguments.
"""
parser.add_argument(
'--courses',
dest='courses',
nargs='+',
help='List of (space separated) courses that need grades computed.',
)
parser.add_argument(
'--all_courses',
help='Compute grades for all courses.',
action='store_true',
default=False,
)
parser.add_argument(
'--from_settings',
help='Compute grades with settings set via Django admin',
action='store_true',
default=False,
)
parser.add_argument(
'--routing_key',
dest='routing_key',
help='Celery routing key to use.',
)
parser.add_argument(
'--batch_size',
help='Maximum number of students to calculate grades for, per celery task.',
default=100,
type=int,
)
parser.add_argument(
'--start_index',
help='Offset from which to start processing enrollments.',
default=0,
type=int,
)
parser.add_argument(
'--no_estimate_first_attempted',
help='Use score data to estimate first_attempted timestamp.',
action='store_false',
dest='estimate_first_attempted',
)
def handle(self, *args, **options):
self._set_log_level(options)
self.enqueue_all_shuffled_tasks(options)
def enqueue_all_shuffled_tasks(self, options):
"""
Enqueue all tasks, in shuffled order.
"""
task_options = {'queue': options['routing_key']} if options.get('routing_key') else {}
for seq_id, kwargs in enumerate(self._shuffled_task_kwargs(options)):
kwargs['seq_id'] = seq_id
result = tasks.compute_grades_for_course_v2.apply_async(kwargs=kwargs, **task_options)
log.info("Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
task_name=tasks.compute_grades_for_course.name,
task_id=result.task_id,
kwargs=kwargs,
))
def _shuffled_task_kwargs(self, options):
"""
Iterate over all task keyword arguments in random order.
Randomizing them will help even out the load on the task workers,
though it will not entirely prevent the possibility of spikes. It will
also make the overall time to completion more predictable.
"""
all_args = []
estimate_first_attempted = options['estimate_first_attempted']
for course_key in self._get_course_keys(options):
# This is a tuple to reduce memory consumption.
# The dictionaries with their extra overhead will be created
# and consumed one at a time.
for task_arg_tuple in tasks._course_task_args(course_key, **options): # lint-amnesty, pylint: disable=protected-access
all_args.append(task_arg_tuple)
all_args.sort(key=lambda x: hashlib.md5(f'{x!r}'.encode('utf-8')).digest())
for args in all_args:
yield {
'course_key': args[0],
'offset': args[1],
'batch_size': args[2],
'estimate_first_attempted': estimate_first_attempted,
}
def _get_course_keys(self, options):
"""
Return a list of courses that need scores computed.
"""
courses_mode = get_mutually_exclusive_required_option(options, 'courses', 'all_courses', 'from_settings')
if courses_mode == 'all_courses':
course_keys = [course.id for course in modulestore().get_course_summaries()]
elif courses_mode == 'courses':
course_keys = parse_course_keys(options['courses'])
else:
course_keys = parse_course_keys(self._latest_settings().course_ids.split())
return course_keys
def _set_log_level(self, options):
"""
Sets logging levels for this module and the block structure
cache module, based on the given the options.
"""
if options.get('verbosity') == 0:
log_level = logging.WARNING
elif options.get('verbosity') >= 1:
log_level = logging.INFO
log.setLevel(log_level)
def _latest_settings(self):
"""
Return the latest version of the ComputeGradesSetting
"""
return ComputeGradesSetting.current()