From 78edd8522d3722d97f72e3312dadac527779ba0f Mon Sep 17 00:00:00 2001 From: Sanford Student Date: Wed, 17 May 2017 14:20:45 -0400 Subject: [PATCH] add command --- .../commands/recalculate_subsection_grades.py | 91 +++++++++++++++++++ .../test_recalculate_subsection_grades.py | 78 ++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py create mode 100644 lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py diff --git a/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py b/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py new file mode 100644 index 0000000000..9384a3066b --- /dev/null +++ b/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py @@ -0,0 +1,91 @@ +""" +Command to recalculate grades for all subsections with problem submissions +in the specified time range. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + +from datetime import datetime +import logging +from pytz import utc + +from django.core.management.base import BaseCommand, CommandError +from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum +from lms.djangoapps.grades.signals.handlers import PROBLEM_SUBMITTED_EVENT_TYPE +from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3 +from courseware.models import StudentModule +from student.models import user_by_anonymous_id +from submissions.models import Submission +from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type +from util.date_utils import to_timestamp + +log = logging.getLogger(__name__) + +DATE_FORMAT = "%Y-%m-%d %H:%M" + + +class Command(BaseCommand): + """ + Example usage: + $ ./manage.py lms recalculate_subsection_grades + --modified_start '2016-08-23 16:43' --modified_end '2016-08-25 16:43' --settings=devstack + """ + args = 'fill this in' + help = 'Recalculates subsection grades for all subsections modified within the given time range.' + + def add_arguments(self, parser): + """ + Entry point for subclassed commands to add custom arguments. + """ + parser.add_argument( + '--modified_start', + dest='modified_start', + help='Starting range for modified date (inclusive): e.g. "2016-08-23 16:43"; expected in UTC.', + ) + parser.add_argument( + '--modified_end', + dest='modified_end', + help='Ending range for modified date (inclusive): e.g. "2016-12-23 16:43"; expected in UTC.', + ) + + def handle(self, *args, **options): + if 'modified_start' not in options: + raise CommandError('modified_start must be provided.') + + if 'modified_end' not in options: + raise CommandError('modified_end must be provided.') + + modified_start = utc.localize(datetime.strptime(options['modified_start'], DATE_FORMAT)) + modified_end = utc.localize(datetime.strptime(options['modified_end'], DATE_FORMAT)) + event_transaction_id = create_new_event_transaction_id() + set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE) + kwargs = {'modified__range': (modified_start, modified_end), 'module_type': 'problem'} + for record in StudentModule.objects.filter(**kwargs): + task_args = { + "user_id": record.student_id, + "course_id": unicode(record.course_id), + "usage_id": unicode(record.module_state_key), + "only_if_higher": False, + "expected_modified_time": to_timestamp(record.modified), + "score_deleted": False, + "event_transaction_id": unicode(event_transaction_id), + "event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE, + "score_db_table": ScoreDatabaseTableEnum.courseware_student_module, + } + recalculate_subsection_grade_v3.apply_async(kwargs=task_args) + + kwargs = {'created_at__range': (modified_start, modified_end)} + for record in Submission.objects.filter(**kwargs): + task_args = { + "user_id": user_by_anonymous_id(record.student_item.student_id).id, + "anonymous_user_id": record.student_item.student_id, + "course_id": unicode(record.student_item.course_id), + "usage_id": unicode(record.student_item.item_id), + "only_if_higher": False, + "expected_modified_time": to_timestamp(record.created_at), + "score_deleted": False, + "event_transaction_id": unicode(event_transaction_id), + "event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE, + "score_db_table": ScoreDatabaseTableEnum.submissions, + } + recalculate_subsection_grade_v3.apply_async(kwargs=task_args) diff --git a/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py b/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py new file mode 100644 index 0000000000..2e57da10ef --- /dev/null +++ b/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py @@ -0,0 +1,78 @@ +""" +Tests for reset_grades management command. +""" + +import ddt +from datetime import datetime +from django.conf import settings +from mock import patch, MagicMock +from pytz import utc + +from lms.djangoapps.grades.management.commands import recalculate_subsection_grades +from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum +from lms.djangoapps.grades.tests.test_tasks import HasCourseWithProblemsMixin +from track.event_transaction_utils import get_event_transaction_id +from util.date_utils import to_timestamp +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +DATE_FORMAT = "%Y-%m-%d %H:%M" + + +@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False}) +@ddt.ddt +class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTestCase): + """ + Tests recalculate subsection grades management command. + """ + def setUp(self): + super(TestRecalculateSubsectionGrades, self).setUp() + self.command = recalculate_subsection_grades.Command() + + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.Submission') + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.user_by_anonymous_id') + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.recalculate_subsection_grade_v3') + def test_submissions(self, task_mock, id_mock, subs_mock): + submission = MagicMock() + submission.student_item = MagicMock( + student_id="anonymousID", + course_id='x/y/z', + item_id='abc', + ) + submission.created_at = utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT)) + subs_mock.objects.filter.return_value = [submission] + id_mock.return_value = MagicMock() + id_mock.return_value.id = "ID" + self._run_command_and_check_output(task_mock, ScoreDatabaseTableEnum.submissions, include_anonymous_id=True) + + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.StudentModule') + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.user_by_anonymous_id') + @patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.recalculate_subsection_grade_v3') + def test_csm(self, task_mock, id_mock, csm_mock): + csm_record = MagicMock() + csm_record.student_id = "ID" + csm_record.course_id = "x/y/z" + csm_record.module_state_key = "abc" + csm_record.modified = utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT)) + csm_mock.objects.filter.return_value = [csm_record] + id_mock.return_value = MagicMock() + id_mock.return_value.id = "ID" + self._run_command_and_check_output(task_mock, ScoreDatabaseTableEnum.courseware_student_module) + + def _run_command_and_check_output(self, task_mock, score_db_table, include_anonymous_id=False): + self.command.handle(modified_start='2016-08-25 16:42', modified_end='2018-08-25 16:44') + kwargs = { + "user_id": "ID", + "course_id": u'x/y/z', + "usage_id": u'abc', + "only_if_higher": False, + "expected_modified_time": to_timestamp(utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT))), + "score_deleted": False, + "event_transaction_id": unicode(get_event_transaction_id()), + "event_transaction_type": u'edx.grades.problem.submitted', + "score_db_table": score_db_table, + } + + if include_anonymous_id: + kwargs['anonymous_user_id'] = 'anonymousID' + + task_mock.apply_async.assert_called_with(kwargs=kwargs)