Generate grade report for verified learnes by default

This commit is contained in:
Awais Jibran
2020-01-23 19:07:59 +05:00
parent 25baa93af9
commit d7d258cd36
4 changed files with 77 additions and 104 deletions

View File

@@ -1016,12 +1016,16 @@ class CourseEnrollmentManager(models.Manager):
def users_enrolled_in(self, course_id, include_inactive=False, verified_only=False): def users_enrolled_in(self, course_id, include_inactive=False, verified_only=False):
""" """
Return a queryset of User for every user enrolled in the course. If Return a queryset of User for every user enrolled in the course.
`include_inactive` is True, returns both active and inactive enrollees
for the course. Otherwise returns actively enrolled users only. Arguments:
If 'verified_only' is True, returns report only for verified enrollees. course_id (CourseLocator): course_id to return enrollees for.
include_inactive (boolean): is a boolean when True, returns both active and inactive enrollees
verified_only (boolean): is a boolean when True, returns only verified enrollees.
Returns:
Returns a User queryset.
""" """
# enrolled
filter_kwargs = { filter_kwargs = {
'courseenrollment__course_id': course_id, 'courseenrollment__course_id': course_id,
} }

View File

@@ -4,7 +4,7 @@ waffle switches for the instructor_task app.
""" """
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleSwitchNamespace
WAFFLE_NAMESPACE = u'instructor_task' WAFFLE_NAMESPACE = u'instructor_task'
INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=WAFFLE_NAMESPACE) INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=WAFFLE_NAMESPACE)
@@ -12,46 +12,14 @@ WAFFLE_SWITCHES = WaffleSwitchNamespace(name=WAFFLE_NAMESPACE)
# Waffle switches # Waffle switches
OPTIMIZE_GET_LEARNERS_FOR_COURSE = u'optimize_get_learners_for_course' OPTIMIZE_GET_LEARNERS_FOR_COURSE = u'optimize_get_learners_for_course'
GENERATE_GRADE_REPORT_VERIFIED_ONLY = u'generate_grade_report_for_verified_only'
# Course-specific flags
PROBLEM_GRADE_REPORT_VERIFIED_ONLY = u'problem_grade_report_verified_only'
COURSE_GRADE_REPORT_VERIFIED_ONLY = u'course_grade_report_verified_only'
def waffle_flags(): def waffle_flags():
""" """
Returns the namespaced, cached, audited Waffle flags dictionary for Grades. Returns the namespaced, cached, audited Waffle flags dictionary for Grades.
""" """
return { return {}
PROBLEM_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag(
waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE,
flag_name=PROBLEM_GRADE_REPORT_VERIFIED_ONLY,
flag_undefined_default=False,
),
COURSE_GRADE_REPORT_VERIFIED_ONLY: CourseWaffleFlag(
waffle_namespace=INSTRUCTOR_TASK_WAFFLE_FLAG_NAMESPACE,
flag_name=COURSE_GRADE_REPORT_VERIFIED_ONLY,
flag_undefined_default=False,
),
}
def problem_grade_report_verified_only(course_id):
"""
Returns True if problem grade reports should only
return rows for verified students in the given course,
False otherwise.
"""
return waffle_flags()[PROBLEM_GRADE_REPORT_VERIFIED_ONLY].is_enabled(course_id)
def course_grade_report_verified_only(course_id):
"""
Returns True if problem grade reports should only
return rows for verified students in the given course,
False otherwise.
"""
return waffle_flags()[PROBLEM_GRADE_REPORT_VERIFIED_ONLY].is_enabled(course_id)
def optimize_get_learners_switch_enabled(): def optimize_get_learners_switch_enabled():
@@ -59,3 +27,11 @@ def optimize_get_learners_switch_enabled():
Returns True if optimize get learner switch is enabled, otherwise False. Returns True if optimize get learner switch is enabled, otherwise False.
""" """
return WAFFLE_SWITCHES.is_enabled(OPTIMIZE_GET_LEARNERS_FOR_COURSE) return WAFFLE_SWITCHES.is_enabled(OPTIMIZE_GET_LEARNERS_FOR_COURSE)
def generate_grade_report_for_verified_only():
"""
Returns True if waffle switch is enabled that indicates generate grading reports only for
verified learners.
"""
return WAFFLE_SWITCHES.is_enabled(GENERATE_GRADE_REPORT_VERIFIED_ONLY)

View File

@@ -29,9 +29,8 @@ from lms.djangoapps.grades.api import prefetch_course_and_subsection_grades
from lms.djangoapps.instructor_analytics.basic import list_problem_responses from lms.djangoapps.instructor_analytics.basic import list_problem_responses
from lms.djangoapps.instructor_analytics.csvs import format_dictlist from lms.djangoapps.instructor_analytics.csvs import format_dictlist
from lms.djangoapps.instructor_task.config.waffle import ( from lms.djangoapps.instructor_task.config.waffle import (
course_grade_report_verified_only, generate_grade_report_for_verified_only,
optimize_get_learners_switch_enabled, optimize_get_learners_switch_enabled
problem_grade_report_verified_only
) )
from lms.djangoapps.teams.models import CourseTeamMembership from lms.djangoapps.teams.models import CourseTeamMembership
from lms.djangoapps.verify_student.services import IDVerificationService from lms.djangoapps.verify_student.services import IDVerificationService
@@ -311,8 +310,9 @@ class CourseGradeReport(object):
""" """
Get enrolled learners in a course. Get enrolled learners in a course.
verified_only(bool): It indicates if we need only the verified Arguments:
enrollments or all enrollments. course_id (CourseLocator): course_id to return enrollees for.
verified_only (boolean): is a boolean when True, returns only verified enrollees.
""" """
if optimize_get_learners_switch_enabled(): if optimize_get_learners_switch_enabled():
TASK_LOG.info(u'%s, Creating Course Grade with optimization', task_log_message) TASK_LOG.info(u'%s, Creating Course Grade with optimization', task_log_message)
@@ -365,8 +365,8 @@ class CourseGradeReport(object):
course_id = context.course_id course_id = context.course_id
task_log_message = u'{}, Task type: {}'.format(context.task_info_string, context.action_name) task_log_message = u'{}, Task type: {}'.format(context.task_info_string, context.action_name)
verified_users_only = course_grade_report_verified_only(course_id) report_for_verified_only = generate_grade_report_for_verified_only()
return get_enrolled_learners_for_course(course_id, verified_users_only) return get_enrolled_learners_for_course(course_id=course_id, verified_only=report_for_verified_only)
def _user_grades(self, course_grade, context): def _user_grades(self, course_grade, context):
""" """
@@ -543,10 +543,11 @@ class ProblemGradeReport(object):
status_interval = 100 status_interval = 100
task_id = _xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None task_id = _xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None
report_for_verified_only = generate_grade_report_for_verified_only()
enrolled_students = CourseEnrollment.objects.users_enrolled_in( enrolled_students = CourseEnrollment.objects.users_enrolled_in(
course_id, course_id=course_id,
include_inactive=True, include_inactive=True,
verified_only=problem_grade_report_verified_only(course_id), verified_only=report_for_verified_only,
) )
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)

View File

@@ -15,36 +15,30 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta from datetime import datetime, timedelta
from six import text_type
from six.moves import range, zip
from six.moves.urllib.parse import quote # pylint: disable=import-error
import ddt import ddt
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
import unicodecsv import unicodecsv
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from django.conf import settings from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
from edx_django_utils.cache import RequestCache from edx_django_utils.cache import RequestCache
from freezegun import freeze_time from freezegun import freeze_time
from mock import ANY, MagicMock, Mock, patch
from pytz import UTC
from six import text_type
from six.moves import range, zip
from six.moves.urllib.parse import quote # pylint: disable=import-error
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.courseware.tests.factories import InstructorFactory
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
from lms.djangoapps.certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory from lms.djangoapps.certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory
from lms.djangoapps.courseware.tests.factories import InstructorFactory
from lms.djangoapps.grades.course_data import CourseData from lms.djangoapps.grades.course_data import CourseData
from lms.djangoapps.grades.models import ( from lms.djangoapps.grades.models import PersistentCourseGrade, PersistentSubsectionGradeOverride
PersistentCourseGrade,
PersistentSubsectionGradeOverride,
)
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
from lms.djangoapps.grades.transformer import GradesTransformer from lms.djangoapps.grades.transformer import GradesTransformer
from lms.djangoapps.instructor_analytics.basic import UNAVAILABLE, list_problem_responses from lms.djangoapps.instructor_analytics.basic import UNAVAILABLE, list_problem_responses
from lms.djangoapps.instructor_task.tasks_helper.certs import generate_students_certificates from lms.djangoapps.instructor_task.tasks_helper.certs import generate_students_certificates
# from lms.djangoapps.instructor_task.config.waffle import problem_grade_report_verified_only
from lms.djangoapps.instructor_task.tasks_helper.enrollments import ( from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
upload_enrollment_report, upload_enrollment_report,
upload_exec_summary_report, upload_exec_summary_report,
@@ -70,12 +64,14 @@ from lms.djangoapps.instructor_task.tests.test_base import (
) )
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from mock import ANY, MagicMock, Mock, patch
from openedx.core.djangoapps.course_groups.models import CohortMembership, CourseUserGroupPartitionGroup from openedx.core.djangoapps.course_groups.models import CohortMembership, CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from openedx.core.djangoapps.credit.tests.factories import CreditCourseFactory from openedx.core.djangoapps.credit.tests.factories import CreditCourseFactory
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
from openedx.core.lib.teams_config import TeamsConfig from openedx.core.lib.teams_config import TeamsConfig
from pytz import UTC
from shoppingcart.models import ( from shoppingcart.models import (
Coupon, Coupon,
CourseRegistrationCode, CourseRegistrationCode,
@@ -88,19 +84,21 @@ from shoppingcart.models import (
from student.models import ALLOWEDTOENROLL_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit from student.models import ALLOWEDTOENROLL_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from survey.models import SurveyAnswer, SurveyForm from survey.models import SurveyAnswer, SurveyForm
from waffle.testutils import override_switch
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
from ..config.waffle import GENERATE_GRADE_REPORT_VERIFIED_ONLY
from ..models import ReportStore from ..models import ReportStore
from ..tasks_helper.utils import UPDATE_STATUS_FAILED, UPDATE_STATUS_SUCCEEDED from ..tasks_helper.utils import UPDATE_STATUS_FAILED, UPDATE_STATUS_SUCCEEDED
_TEAMS_CONFIG = TeamsConfig({ _TEAMS_CONFIG = TeamsConfig({
'max_size': 2, 'max_size': 2,
'topics': [{'id': 'topic', 'name': 'Topic', 'description': 'A Topic'}], 'topics': [{'id': 'topic', 'name': 'Topic', 'description': 'A Topic'}],
}) })
SWITCH_GENERATE_GRADE_REPORT_VERIFIED_ONLY = '.'.join(['instructor_task', GENERATE_GRADE_REPORT_VERIFIED_ONLY])
class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase): class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase):
@@ -422,7 +420,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
RequestCache.clear_all_namespaces() RequestCache.clear_all_namespaces()
expected_query_count = 51 expected_query_count = 50
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'): with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(mongo_count): with check_mongo_calls(mongo_count):
with self.assertNumQueries(expected_query_count): with self.assertNumQueries(expected_query_count):
@@ -945,26 +943,23 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
]) ])
@patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') @patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
@override_switch(SWITCH_GENERATE_GRADE_REPORT_VERIFIED_ONLY, True)
def test_single_problem_verified_student_only(self, _get_current_task): def test_single_problem_verified_student_only(self, _get_current_task):
with patch( student_verified = self.create_student(u'user_verified', mode='verified')
'lms.djangoapps.instructor_task.tasks_helper.grades.problem_grade_report_verified_only', vertical = ItemFactory.create(
return_value=True, parent_location=self.problem_section.location,
): category='vertical',
student_verified = self.create_student(u'user_verified', mode='verified') metadata={'graded': True},
vertical = ItemFactory.create( display_name='Problem Vertical'
parent_location=self.problem_section.location, )
category='vertical', self.define_option_problem(u'Problem1', parent=vertical)
metadata={'graded': True},
display_name='Problem Vertical'
)
self.define_option_problem(u'Problem1', parent=vertical)
self.submit_student_answer(self.student_1.username, u'Problem1', ['Option 1']) self.submit_student_answer(self.student_1.username, u'Problem1', ['Option 1'])
self.submit_student_answer(student_verified.username, u'Problem1', ['Option 1']) self.submit_student_answer(student_verified.username, u'Problem1', ['Option 1'])
result = ProblemGradeReport.generate(None, None, self.course.id, None, 'graded') result = ProblemGradeReport.generate(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset( self.assertDictContainsSubset(
{'action_name': 'graded', 'attempted': 1, 'succeeded': 1, 'failed': 0}, result {'action_name': 'graded', 'attempted': 1, 'succeeded': 1, 'failed': 0}, result
) )
@patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') @patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
@patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.iter') @patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.iter')
@@ -2039,31 +2034,28 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
) )
@patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') @patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
@override_switch(SWITCH_GENERATE_GRADE_REPORT_VERIFIED_ONLY, True)
def test_course_grade_with_verified_student_only(self, _get_current_task): def test_course_grade_with_verified_student_only(self, _get_current_task):
""" """
Tests that course grade report has expected data when it is generated only for Tests that course grade report has expected data when it is generated only for
verified learners. verified learners.
""" """
with patch( student_1 = self.create_student(u'user_honor')
'lms.djangoapps.instructor_task.tasks_helper.grades.course_grade_report_verified_only', student_verified = self.create_student(u'user_verified', mode='verified')
return_value=True, vertical = ItemFactory.create(
): parent_location=self.problem_section.location,
student_1 = self.create_student(u'user_honor') category='vertical',
student_verified = self.create_student(u'user_verified', mode='verified') metadata={'graded': True},
vertical = ItemFactory.create( display_name='Problem Vertical'
parent_location=self.problem_section.location, )
category='vertical', self.define_option_problem(u'Problem1', parent=vertical)
metadata={'graded': True},
display_name='Problem Vertical'
)
self.define_option_problem(u'Problem1', parent=vertical)
self.submit_student_answer(student_1.username, u'Problem1', ['Option 1']) self.submit_student_answer(student_1.username, u'Problem1', ['Option 1'])
self.submit_student_answer(student_verified.username, u'Problem1', ['Option 1']) self.submit_student_answer(student_verified.username, u'Problem1', ['Option 1'])
result = CourseGradeReport.generate(None, None, self.course.id, None, 'graded') result = CourseGradeReport.generate(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset( self.assertDictContainsSubset(
{'action_name': 'graded', 'attempted': 1, 'succeeded': 1, 'failed': 0}, result {'action_name': 'graded', 'attempted': 1, 'succeeded': 1, 'failed': 0}, result
) )
@ddt.data(True, False) @ddt.data(True, False)
def test_fast_generation(self, create_non_zero_grade): def test_fast_generation(self, create_non_zero_grade):