def get_user_course_response_task(users, course_str, depth, callback_url): """ Get a list of users grades' for a course """ user_grades = {} grades_schema = {} course_key = CourseKey.from_string(str(course_str)) course = courses.get_course(course_key) for user in users: course_grade = CourseGradeFactory().update(user, course) if depth == "all": grades_schema = get_user_grades(user.id, course_str) else: grades_schema = "Showing course grade summary, specify depth=all in query params." user_grades[user.username] = { 'name': "{} {}".format(user.first_name, user.last_name), 'email': user.email, 'start_date': course.start, 'end_date': course.end if not None else "This course has no end date.", 'all_grades': grades_schema, "passed": course_grade.passed, "percent": course_grade.percent } #requests.post(str(callback_url), data=user_grades) return user_grades
def send_xapi_statements(self, lrs_configuration, days): """ Send xAPI analytics data of the enterprise learners to the given LRS. Arguments: lrs_configuration (XAPILRSConfiguration): Configuration object containing LRS configurations of the LRS where to send xAPI learner analytics. days (int): Include course enrollment of this number of days. """ persistent_course_grades = self.get_course_completions( lrs_configuration.enterprise_customer, days) users = self.prefetch_users(persistent_course_grades) course_overviews = self.prefetch_courses(persistent_course_grades) for persistent_course_grade in persistent_course_grades: try: user = users.get(persistent_course_grade.user_id) course_overview = course_overviews.get( persistent_course_grade.course_id) course_grade = CourseGradeFactory().read( user, course_key=persistent_course_grade.course_id) send_course_completion_statement(lrs_configuration, user, course_overview, course_grade) except ClientError: LOGGER.exception( 'Client error while sending course completion to xAPI for' ' enterprise customer {enterprise_customer}.'.format( enterprise_customer=lrs_configuration. enterprise_customer.name))
def get_course_grades(user, course): """ Gets course grades for a given student """ grades = CourseGradeFactory().read(user, course) return grades
def test_cert_already_generated(self): with mock.patch( 'lms.djangoapps.certificates.signals.generate_certificate.apply_async', return_value=None ) as mock_generate_certificate_apply_async: grade_factory = CourseGradeFactory() # Create the certificate GeneratedCertificate.eligible_certificates.create( user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable ) # Certs are not re-fired after passing with mock_passing_grade(): grade_factory.update(self.user, self.course) mock_generate_certificate_apply_async.assert_not_called()
def get_course_info_for_application_review(courses, user): """ Get courses info for application review page. Args: courses (list): List of courses user (User): User object Returns: dict: Contains course name and percent grade | status """ courses_info = {} for course in courses: if not CourseEnrollment.is_enrolled(user, course.id): courses_info[course.display_name] = NOT_STARTED continue grade = CourseGradeFactory().read(user, course_key=course.id) if grade.passed: courses_info[ course. display_name] = f'{convert_float_point_to_percentage(grade.percent)}%' continue courses_info[course.display_name] = IN_PROGRESS return courses_info
def check_state(self, user, descriptor, expected_score, expected_max_score, expected_attempts=1): """ Check that the StudentModule state contains the expected values. The student module is found for the test course, given the `username` and problem `descriptor`. Values checked include the number of attempts, the score, and the max score for a problem. """ module = self.get_student_module(user.username, descriptor) self.assertEqual(module.grade, expected_score) self.assertEqual(module.max_grade, expected_max_score) state = json.loads(module.state) attempts = state['attempts'] self.assertEqual(attempts, expected_attempts) if attempts > 0: self.assertIn('correct_map', state) self.assertIn('student_answers', state) self.assertGreater(len(state['correct_map']), 0) self.assertGreater(len(state['student_answers']), 0) # assume only one problem in the subsection and the grades # are in sync. expected_subsection_grade = expected_score course_grade = CourseGradeFactory().read(user, self.course) self.assertEquals( course_grade.graded_subsections_by_format['Homework'][ self.problem_section.location].graded_total.earned, expected_subsection_grade, )
def get_grade_book_page(request, course, course_key): """ Get student records per page along with page information i.e current page, total pages and offset information. """ # Unsanitized offset current_offset = request.GET.get('offset', 0) enrolled_students = User.objects.filter( courseenrollment__course_id=course_key, courseenrollment__is_active=1 ).order_by('username').select_related("profile") total_students = enrolled_students.count() page = calculate_page_info(current_offset, total_students) offset = page["offset"] total_pages = page["total_pages"] if total_pages > 1: # Apply limit on queryset only if total number of students are greater then MAX_STUDENTS_PER_PAGE_GRADE_BOOK. enrolled_students = enrolled_students[offset: offset + MAX_STUDENTS_PER_PAGE_GRADE_BOOK] with modulestore().bulk_operations(course.location.course_key): student_info = [ { 'username': student.username, 'id': student.id, 'email': student.email, 'grade_summary': CourseGradeFactory().create(student, course).summary } for student in enrolled_students ] return student_info, page
def __init__(self, site, user, enrollments=None, uuid=None): self.site = site self.user = user self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = unicode(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) self.course_grade_factory = CourseGradeFactory() if uuid: self.programs = [get_programs(self.site, uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs(self.site))
def _get_single_user_grade(self, request, course_key): """ Returns a grade response for the user object corresponding to the request's 'username' parameter, or the current request.user if no 'username' was provided. Args: request (Request): django request object to check for username or request.user object course_key (CourseLocator): The course to retrieve user grades for. Returns: A serializable list of grade responses """ if 'username' in request.GET: username = request.GET.get('username') else: username = request.user.username grade_user = USER_MODEL.objects.get(username=username) if not enrollment_data.get_course_enrollment(username, str(course_key)): raise CourseEnrollment.DoesNotExist course_grade = CourseGradeFactory().read(grade_user, course_key=course_key) return Response( [self._make_grade_response(grade_user, course_key, course_grade)])
def test_grade_changed(self, mock_send_grade_if_interesting): user = UserFactory() course = XModuleCourseFactory() self.assertFalse(mock_send_grade_if_interesting.called) CourseGradeFactory().update(user, course=course) self.assertTrue(mock_send_grade_if_interesting.called)
def get(self, request, course_id): """ Returns a gradebook entry/entries (i.e. both course and subsection-level grade data) for all users enrolled in a course, or a single user enrolled in a course if a `username` parameter is provided. Args: request: A Django request object. course_id: A string representation of a CourseKey object. """ username = request.GET.get('username') course_key = get_course_key(request, course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) if username: with self._get_user_or_raise(request, course_id) as grade_user: course_grade = CourseGradeFactory().read(grade_user, course) entry = self._gradebook_entry(grade_user, course, course_grade) serializer = StudentGradebookEntrySerializer(entry) return Response(serializer.data) else: # list gradebook data for all course enrollees entries = [] for user, course_grade, exc in self._iter_user_grades(course_key): if not exc: entries.append(self._gradebook_entry(user, course, course_grade)) serializer = StudentGradebookEntrySerializer(entries, many=True) return self.get_paginated_response(serializer.data)
def test_grade_changed(self, mock_send_grade_if_interesting): user = UserFactory() course = XModuleCourseFactory() assert not mock_send_grade_if_interesting.called CourseGradeFactory().update(user, course=course) assert mock_send_grade_if_interesting.called
def get(self, request, course_id): """ Gets a course progress status. Args: request (Request): Django request object. course_id (string): URI element specifying the course location. Return: A JSON serialized representation of the requesting user's current grade status. """ course = self._get_course(course_id, request.user, 'load') if isinstance(course, Response): # Returns a 404 if course_id is invalid, or request.user is not enrolled in the course return course grade_user = self._get_effective_user(request, course) if isinstance(grade_user, Response): # Returns a 403 if the request.user can't access grades for the requested user, # or a 404 if the requested user does not exist. return grade_user prep_course_for_grading(course, request) course_grade = CourseGradeFactory().create(grade_user, course) return Response([{ 'username': grade_user.username, 'course_key': course_id, 'passed': course_grade.passed, 'percent': course_grade.percent, 'letter_grade': course_grade.letter_grade, }])
def _rows_for_users(self, context, users): """ Returns a list of rows for the given users for this report. """ with modulestore().bulk_operations(context.course_id): bulk_context = _CourseGradeBulkContext(context, users) success_rows, error_rows = [], [] for user, course_grade, error in CourseGradeFactory().iter( users, course=context.course, collected_block_structure=context.course_structure, course_key=context.course_id, ): if not course_grade: # An empty gradeset means we failed to grade a student. error_rows.append( [user.id, user.username, text_type(error)]) else: success_rows.append( [user.id, user.email, user.username] + self._user_grades(course_grade, context) + self._user_cohort_group_names(user, context) + self._user_experiment_group_names(user, context) + self._user_team_names(user, bulk_context.teams) + self._user_verification_mode( user, context, bulk_context.enrollments) + self._user_certificate_info( user, context, course_grade, bulk_context.certs) + [_user_enrollment_status(user, context.course_id)]) return success_rows, error_rows
def setUp(self): super(TestGradeUcursosExportView, self).setUp() self.grade_factory = CourseGradeFactory() with patch('student.models.cc.User.save'): # staff user self.client_instructor = Client() self.client_student = Client() self.client_anonymous = Client() self.user_instructor = UserFactory( username='******', password='******', email='*****@*****.**', is_staff=True) role = CourseInstructorRole(self.course.id) role.add_users(self.user_instructor) self.client_instructor.login( username='******', password='******') self.student = UserFactory( username='******', password='******', email='*****@*****.**') self.student_2 = UserFactory( username='******', password='******', email='*****@*****.**') # Enroll the student in the course CourseEnrollmentFactory( user=self.student, course_id=self.course.id, mode='honor') CourseEnrollmentFactory( user=self.student_2, course_id=self.course.id, mode='honor') self.client_student.login( username='******', password='******')
def _iter_user_grades(self, course_key, course_enrollment_filter=None, related_models=None): """ Args: course_key (CourseLocator): The course to retrieve grades for. course_enrollment_filter: Optional dictionary of keyword arguments to pass to `CourseEnrollment.filter()`. related_models: Optional list of related models to join to the CourseEnrollment table. Returns: An iterator of CourseGrade objects for users enrolled in the given course. """ filter_kwargs = { 'course_id': course_key, 'is_active': True, } filter_kwargs.update(course_enrollment_filter or {}) enrollments_in_course = CourseEnrollment.objects.filter( **filter_kwargs) if related_models: enrollments_in_course = enrollments_in_course.select_related( *related_models) paged_enrollments = self.paginate_queryset(enrollments_in_course) users = (enrollment.user for enrollment in paged_enrollments) grades = CourseGradeFactory().iter(users, course_key=course_key) for user, course_grade, exc in grades: yield user, course_grade, exc
def test_cert_already_generated_unverified(self): with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True): GeneratedCertificateFactory( user=self.user, course_id=self.course.id, status=CertificateStatuses.unverified ) with mock.patch( 'lms.djangoapps.certificates.signals.generate_certificate_task', return_value=None ) as mock_cert_task: grade_factory = CourseGradeFactory() with mock_passing_grade(): grade_factory.update(self.user, self.course) mock_cert_task.assert_called_with(self.user, self.course_key)
def test_user_has_passing_grade(self): CourseEnrollment.enroll(self.user, self.course.id) self.course._grading_policy['GRADE_CUTOFFS']['Pass'] = 0 # pylint: disable=protected-access self.update_course(self.course, self.user.id) CourseGradeFactory().update(self.user, self.course) response = self.client.get(self.url) assert response.status_code == 200 assert response.data['user_has_passing_grade'] is True
def send_composite_outcome(user_id, course_id, assignment_id, version): """ Calculate and transmit the score for a composite module (such as a vertical). A composite module may contain multiple problems, so we need to calculate the total points earned and possible for all child problems. This requires calculating the scores for the whole course, which is an expensive operation. Callers should be aware that the score calculation code accesses the latest scores from the database. This can lead to a race condition between a view that updates a user's score and the calculation of the grade. If the Celery task attempts to read the score from the database before the view exits (and its transaction is committed), it will see a stale value. Care should be taken that this task is not triggered until the view exits. The GradedAssignment model has a version_number field that is incremented whenever the score is updated. It is used by this method for two purposes. First, it allows the task to exit if it detects that it has been superseded by another task that will transmit the score for the same assignment. Second, it prevents a race condition where two tasks calculate different scores for a single assignment, and may potentially update the campus LMS in the wrong order. """ assignment = GradedAssignment.objects.get(id=assignment_id) if version != assignment.version_number: log.info( "Score passback for GradedAssignment %s skipped. More recent score available.", assignment.id ) return course_key = CourseKey.from_string(course_id) mapped_usage_key = assignment.usage_key.map_into_course(course_key) user = User.objects.get(id=user_id) course = modulestore().get_course(course_key, depth=0) course_grade = CourseGradeFactory().read(user, course) earned, possible = course_grade.score_for_module(mapped_usage_key) if possible == 0: weighted_score = 0 else: weighted_score = float(earned) / float(possible) assignment = GradedAssignment.objects.get(id=assignment_id) if assignment.version_number == version: outcomes.send_score_update(assignment, weighted_score)
def _get_course_grade_passed(user, course_id): """ Get 'passed' (Boolean representing whether the course has been passed according to the course's grading policy.) """ course_key = CourseKey.from_string(course_id) course_grade = CourseGradeFactory().read(user, course_key=course_key) return course_grade.passed
def send_composite_outcome(user_id, course_id, assignment_id, version): """ Calculate and transmit the score for a composite module (such as a vertical). A composite module may contain multiple problems, so we need to calculate the total points earned and possible for all child problems. This requires calculating the scores for the whole course, which is an expensive operation. Callers should be aware that the score calculation code accesses the latest scores from the database. This can lead to a race condition between a view that updates a user's score and the calculation of the grade. If the Celery task attempts to read the score from the database before the view exits (and its transaction is committed), it will see a stale value. Care should be taken that this task is not triggered until the view exits. The GradedAssignment model has a version_number field that is incremented whenever the score is updated. It is used by this method for two purposes. First, it allows the task to exit if it detects that it has been superseded by another task that will transmit the score for the same assignment. Second, it prevents a race condition where two tasks calculate different scores for a single assignment, and may potentially update the campus LMS in the wrong order. """ assignment = GradedAssignment.objects.get(id=assignment_id) if version != assignment.version_number: log.info( "Score passback for GradedAssignment %s skipped. More recent score available.", assignment.id) return course_key = CourseKey.from_string(course_id) mapped_usage_key = assignment.usage_key.map_into_course(course_key) user = User.objects.get(id=user_id) course = modulestore().get_course(course_key, depth=0) course_grade = CourseGradeFactory().create(user, course) earned, possible = course_grade.score_for_module(mapped_usage_key) if possible == 0: weighted_score = 0 else: weighted_score = float(earned) / float(possible) assignment = GradedAssignment.objects.get(id=assignment_id) if assignment.version_number == version: outcomes.send_score_update(assignment, weighted_score)
def get(self, request, course_id): """ Returns a gradebook entry/entries (i.e. both course and subsection-level grade data) for all users enrolled in a course, or a single user enrolled in a course if a `username` parameter is provided. Args: request: A Django request object. course_id: A string representation of a CourseKey object. """ course_key = get_course_key(request, course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) if request.GET.get('username'): with self._get_user_or_raise(request, course_key) as grade_user: course_grade = CourseGradeFactory().read(grade_user, course) entry = self._gradebook_entry(grade_user, course, course_grade) serializer = StudentGradebookEntrySerializer(entry) return Response(serializer.data) else: filter_kwargs = {} related_models = [] if request.GET.get('username_contains'): filter_kwargs['user__username__icontains'] = request.GET.get('username_contains') related_models.append('user') if request.GET.get('cohort_id'): cohort = cohorts.get_cohort_by_id(course_key, request.GET.get('cohort_id')) if cohort: filter_kwargs['user__in'] = cohort.users.all() else: filter_kwargs['user__in'] = [] if request.GET.get('enrollment_mode'): filter_kwargs['mode'] = request.GET.get('enrollment_mode') entries = [] users = self._paginate_users(course_key, filter_kwargs, related_models) with bulk_gradebook_view_context(course_key, users): for user, course_grade, exc in CourseGradeFactory().iter(users, course_key=course_key): if not exc: entries.append(self._gradebook_entry(user, course, course_grade)) serializer = StudentGradebookEntrySerializer(entries, many=True) return self.get_paginated_response(serializer.data)
def _add_entrance_exam_to_context(self, courseware_context): """ Adds entrance exam related information to the given context. """ if course_has_entrance_exam(self.course) and getattr(self.chapter, 'is_entrance_exam', False): courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.effective_user, self.course) courseware_context['entrance_exam_current_score'] = get_entrance_exam_score_ratio( CourseGradeFactory().create(self.effective_user, self.course), get_entrance_exam_usage_key(self.course), )
def user_course_passed(self): from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from django.contrib.auth.models import User try: user = User.objects.get(id=self.scope_ids.user_id) response = CourseGradeFactory().read(user, course_key=self.course_id) return response.passed except User.DoesNotExist: return False
def get_grade_data(self, user, course_key, grade_cutoffs): """ Collects and formats the grades data for a particular user and course. Args: user (User) course_key (CourseKey) grade_cutoffs: # TODO: LEARNER-3854: Complete docstring if implementing Learner Analytics. """ course_grade = CourseGradeFactory().read(user, course_key=course_key) grades = [] total_earned = 0 total_possible = 0 # answered_percent seems to be unused and it does not take into account assignment type weightings answered_percent = None chapter_grades = course_grade.chapter_grades.values() for chapter in chapter_grades: # Note: this code exists on the progress page. We should be able to remove it going forward. if not chapter['display_name'] == "hidden": for subsection_grade in chapter['sections']: log.info(subsection_grade.display_name) possible = subsection_grade.graded_total.possible earned = subsection_grade.graded_total.earned passing_grade = math.ceil(possible * grade_cutoffs['Pass']) grades.append({ 'assignment_type': subsection_grade.format, 'total_earned': earned, 'total_possible': possible, 'passing_grade': passing_grade, 'display_name': subsection_grade.display_name, 'location': unicode(subsection_grade.location), 'assigment_url': reverse('jump_to_id', kwargs={ 'course_id': unicode(course_key), 'module_id': unicode(subsection_grade.location), }) }) if earned > 0: total_earned += earned total_possible += possible if total_possible > 0: answered_percent = float(total_earned) / total_possible return (grades, answered_percent, course_grade.percent)
def send_xapi_statements(self, lrs_configuration, days): """ Send xAPI analytics data of the enterprise learners to the given LRS. Arguments: lrs_configuration (XAPILRSConfiguration): Configuration object containing LRS configurations of the LRS where to send xAPI learner analytics. days (int): Include course enrollment of this number of days. """ persistent_course_grades = self.get_course_completions( lrs_configuration.enterprise_customer, days) users = self.prefetch_users(persistent_course_grades) course_overviews = self.prefetch_courses(persistent_course_grades) for persistent_course_grade in persistent_course_grades: error_message = None user = users.get(persistent_course_grade.user_id) course_overview = course_overviews.get( persistent_course_grade.course_id) course_grade = CourseGradeFactory().read( user, course_key=persistent_course_grade.course_id) xapi_transmission_queryset = XAPILearnerDataTransmissionAudit.objects.filter( user=user, course_id=persistent_course_grade.course_id) if not xapi_transmission_queryset.exists(): LOGGER.warning( 'XAPILearnerDataTransmissionAudit object does not exist for user: {username}, course: ' '{course_id} so skipping the course completion statement to xapi.' ) continue try: send_course_completion_statement(lrs_configuration, user, course_overview, course_grade) except ClientError: error_message = 'Client error while sending course completion to xAPI for ' \ 'enterprise customer: {enterprise_customer}, user: {username} ' \ 'and course: {course_id}'.format( enterprise_customer=lrs_configuration.enterprise_customer.name, username=user.username if user else '', course_id=persistent_course_grade.course_id ) LOGGER.exception(error_message) status = 500 else: LOGGER.info( 'Successfully sent course completion to xAPI for user: {username} for course: {course_id}' .format(username=user.username if user else '', course_id=persistent_course_grade.course_id)) status = 200 fields = {'status': status, 'error_message': error_message} if status == 200: fields.update({ 'grade': course_grade.percent, 'timestamp': course_grade.passed_timestamp }) xapi_transmission_queryset.update(**fields)
def get_course_grades_for_users(user_ids, course_id): """ Returns course grades for the given users and course. Args: user_ids (list): List of user ids. course_id (CourseKeyString): Course id. Returns: dict: Dictionary containing user and grades information. """ grades = PersistentCourseGrade.objects.filter( user_id__in=user_ids, course_id=course_id ).only('user_id', 'percent_grade', 'passed_timestamp') grades_dict = { grade.user_id: { 'grade_percentage': f'{convert_float_point_to_percentage(grade.percent_grade)}%', 'passed_timestamp': grade.passed_timestamp } for grade in grades } if len(user_ids) != len(grades): course_grade_factory = CourseGradeFactory() user_ids_without_grades = [user_id for user_id in user_ids if user_id not in grades_dict.keys()] users = User.objects.filter(id__in=user_ids_without_grades) for user in users: if grades_dict.get(user.id, None): continue course_grade = course_grade_factory.read( user=user, course_key=course_id ) grades_dict[user.id] = { 'grade_percentage': f'{convert_float_point_to_percentage(course_grade.percent)}%', 'passed_timestamp': now() if course_grade.passed else None, } return grades_dict
def user_course_passed(user, course_key): """ Get if user passed course with percert """ response = CourseGradeFactory().read(user, course_key=course_key) if response is None: logger.error( 'CorfoGenerateCode - Error to get CourseGradeFactory().read(...), user: {}, course: {}' .format(user, str(course_key))) return None, None return response.passed, response.percent
def test_cert_failure(self, status): if CertificateStatuses.is_passing_status(status): expected_status = CertificateStatuses.notpassing else: expected_status = status GeneratedCertificate.eligible_certificates.create( user=self.user, course_id=self.course.id, status=status) CourseGradeFactory().update(self.user, self.course) cert = GeneratedCertificate.certificate_for_student( self.user, self.course.id) self.assertEqual(cert.status, expected_status)
def test_cert_generation_on_passing_instructor_paced(self): with mock.patch( 'lms.djangoapps.certificates.signals.generate_certificate.apply_async', return_value=None ) as mock_generate_certificate_apply_async: with waffle.waffle().override(waffle.AUTO_CERTIFICATE_GENERATION, active=True): grade_factory = CourseGradeFactory() # Not passing grade_factory.update(self.user, self.ip_course) mock_generate_certificate_apply_async.assert_not_called() # Certs fired after passing with mock_passing_grade(): grade_factory.update(self.user, self.ip_course) mock_generate_certificate_apply_async.assert_called_with( countdown=CERTIFICATE_DELAY_SECONDS, kwargs={ 'student': unicode(self.user.id), 'course_key': unicode(self.ip_course.id), } )
def setUp(self): super(LearnerTrackChangeCertsTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments self.course_one = CourseFactory.create(self_paced=True) self.user_one = UserFactory.create() self.enrollment_one = CourseEnrollmentFactory( user=self.user_one, course_id=self.course_one.id, is_active=True, mode='verified', ) self.user_two = UserFactory.create() self.course_two = CourseFactory.create(self_paced=False) self.enrollment_two = CourseEnrollmentFactory( user=self.user_two, course_id=self.course_two.id, is_active=True, mode='verified') with mock_passing_grade(): grade_factory = CourseGradeFactory() grade_factory.update(self.user_one, self.course_one) grade_factory.update(self.user_two, self.course_two)
def test_failing_grade_allowlist(self): # User who is not on the allowlist GeneratedCertificateFactory(user=self.user, course_id=self.course.id, status=CertificateStatuses.downloadable) CourseGradeFactory().update(self.user, self.course) cert = GeneratedCertificate.certificate_for_student( self.user, self.course.id) self.assertEqual(cert.status, CertificateStatuses.notpassing) # User who is on the allowlist u = UserFactory.create() c = CourseFactory() course_key = c.id # pylint: disable=no-member CertificateWhitelistFactory(user=u, course_id=course_key) GeneratedCertificateFactory(user=u, course_id=course_key, status=CertificateStatuses.downloadable) CourseGradeFactory().update(u, c) cert = GeneratedCertificate.certificate_for_student(u, course_key) self.assertEqual(cert.status, CertificateStatuses.downloadable)
def assert_course_grade(self, user, expected_percent): """ Verifies the given user's course grade is the expected percentage. Also verifies the user's grade information contains values for all problems in the course, whether or not they are currently gated. """ course_grade = CourseGradeFactory().create(user, self.course) for prob in [self.gating_prob1, self.gated_prob2, self.prob3]: self.assertIn(prob.location, course_grade.problem_scores) self.assertEquals(course_grade.percent, expected_percent)
def setUp(self): super(LearnerTrackChangeCertsTest, self).setUp() self.course_one = CourseFactory.create(self_paced=True) self.user_one = UserFactory.create() self.enrollment_one = CourseEnrollmentFactory( user=self.user_one, course_id=self.course_one.id, is_active=True, mode='verified', ) self.user_two = UserFactory.create() self.course_two = CourseFactory.create(self_paced=False) self.enrollment_two = CourseEnrollmentFactory( user=self.user_two, course_id=self.course_two.id, is_active=True, mode='verified' ) with mock_passing_grade(): grade_factory = CourseGradeFactory() grade_factory.update(self.user_one, self.course_one) grade_factory.update(self.user_two, self.course_two)
def _listen_for_id_verification_status_changed(sender, user, **kwargs): # pylint: disable=unused-argument """ Catches a track change signal, determines user status, calls fire_ungenerated_certificate_task for passing grades """ if not auto_certificate_generation_enabled(): return user_enrollments = CourseEnrollment.enrollments_for_user(user=user) grade_factory = CourseGradeFactory() expected_verification_status, _ = SoftwareSecurePhotoVerification.user_status(user) for enrollment in user_enrollments: if grade_factory.read(user=user, course=enrollment.course_overview).passed: if fire_ungenerated_certificate_task(user, enrollment.course_id, expected_verification_status): message = ( u'Certificate generation task initiated for {user} : {course} via track change ' + u'with verification status of {status}' ) log.info(message.format( user=user.id, course=enrollment.course_id, status=expected_verification_status ))
class ProgramProgressMeter(object): """Utility for gauging a user's progress towards program completion. Arguments: user (User): The user for which to find programs. Keyword Arguments: enrollments (list): List of the user's enrollments. uuid (str): UUID identifying a specific program. If provided, the meter will only inspect this one program, not all programs the user may be engaged with. """ def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False): self.site = site self.user = user self.mobile_only = mobile_only self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = unicode(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user)) self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements] self.course_grade_factory = CourseGradeFactory() if uuid: self.programs = [get_programs(uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only) def invert_programs(self): """Intersect programs and enrollments. Builds a dictionary of program dict lists keyed by course run ID and by course UUID. The resulting dictionary is suitable in applications where programs must be filtered by the course runs or courses they contain (e.g., the student dashboard). Returns: defaultdict, programs keyed by course run ID """ inverted_programs = defaultdict(list) for program in self.programs: for course in program['courses']: course_uuid = course['uuid'] if course_uuid in self.course_uuids: program_list = inverted_programs[course_uuid] if program not in program_list: program_list.append(program) for course_run in course['course_runs']: course_run_id = course_run['key'] if course_run_id in self.course_run_ids: program_list = inverted_programs[course_run_id] if program not in program_list: program_list.append(program) # Sort programs by title for consistent presentation. for program_list in inverted_programs.itervalues(): program_list.sort(key=lambda p: p['title']) return inverted_programs @cached_property def engaged_programs(self): """Derive a list of programs in which the given user is engaged. Returns: list of program dicts, ordered by most recent enrollment """ inverted_programs = self.invert_programs() programs = [] # Remember that these course run ids are derived from a list of # enrollments sorted from most recent to least recent. Iterating # over the values in inverted_programs alone won't yield a program # ordering consistent with the user's enrollments. for course_run_id in self.course_run_ids: for program in inverted_programs[course_run_id]: # Dicts aren't a hashable type, so we can't use a set. Sets also # aren't ordered, which is important here. if program not in programs: programs.append(program) for course_uuid in self.course_uuids: for program in inverted_programs[course_uuid]: if program not in programs: programs.append(program) return programs def _is_course_in_progress(self, now, course): """Check if course qualifies as in progress as part of the program. A course is considered to be in progress if a user is enrolled in a run of the correct mode or a run of the correct mode is still available for enrollment. Arguments: now (datetime): datetime for now course (dict): Containing nested course runs. Returns: bool, indicating whether the course is in progress. """ enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids] # Check if the user is enrolled in a required run and mode/seat. runs_with_required_mode = [ run for run in enrolled_runs if run['type'] == self.enrolled_run_modes[run['key']] ] if runs_with_required_mode: not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs] if not_failed_runs: return True # Check if seats required for course completion are still available. upgrade_deadlines = [] for run in enrolled_runs: for seat in run['seats']: if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]: upgrade_deadlines.append(seat['upgrade_deadline']) # An upgrade deadline of None means the course is always upgradeable. return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines) def progress(self, programs=None, count_only=True): """Gauge a user's progress towards program completion. Keyword Arguments: programs (list): Specific list of programs to check the user's progress against. If left unspecified, self.engaged_programs will be used. count_only (bool): Whether or not to return counts of completed, in progress, and unstarted courses instead of serialized representations of the courses. Returns: list of dict, each containing information about a user's progress towards completing a program. """ now = datetime.datetime.now(utc) progress = [] programs = programs or self.engaged_programs for program in programs: program_copy = deepcopy(program) completed, in_progress, not_started = [], [], [] for course in program_copy['courses']: active_entitlement = CourseEntitlement.get_entitlement_if_active( user=self.user, course_uuid=course['uuid'] ) if self._is_course_complete(course): completed.append(course) elif self._is_course_enrolled(course) or active_entitlement: # Show all currently enrolled courses and active entitlements as in progress if active_entitlement: course['course_runs'] = get_fulfillable_course_runs_for_entitlement( active_entitlement, course['course_runs'] ) course['user_entitlement'] = active_entitlement.to_dict() course['enroll_url'] = reverse( 'entitlements_api:v1:enrollments', args=[str(active_entitlement.uuid)] ) in_progress.append(course) else: course_in_progress = self._is_course_in_progress(now, course) if course_in_progress: in_progress.append(course) else: course['expired'] = not course_in_progress not_started.append(course) else: not_started.append(course) grades = {} for run in self.course_run_ids: grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run)) grades[run] = grade.percent progress.append({ 'uuid': program_copy['uuid'], 'completed': len(completed) if count_only else completed, 'in_progress': len(in_progress) if count_only else in_progress, 'not_started': len(not_started) if count_only else not_started, 'grades': grades, }) return progress @property def completed_programs_with_available_dates(self): """ Calculate the available date for completed programs based on course runs. Returns a dict of {uuid_string: available_datetime} """ # Query for all user certs up front, for performance reasons (rather than querying per course run). user_certificates = GeneratedCertificate.eligible_available_certificates.filter(user=self.user) certificates_by_run = {cert.course_id: cert for cert in user_certificates} completed = {} for program in self.programs: available_date = self._available_date_for_program(program, certificates_by_run) if available_date: completed[program['uuid']] = available_date return completed def _available_date_for_program(self, program_data, certificates): """ Calculate the available date for the program based on the courses within it. Arguments: program_data (dict): nested courses and course runs certificates (dict): course run key -> certificate mapping Returns a datetime object or None if the program is not complete. """ program_available_date = None for course in program_data['courses']: earliest_course_run_date = None for course_run in course['course_runs']: key = CourseKey.from_string(course_run['key']) # Get a certificate if one exists certificate = certificates.get(key) if certificate is None: continue # Modes must match (see _is_course_complete() comments for why) course_run_mode = self._course_run_mode_translation(course_run['type']) certificate_mode = self._certificate_mode_translation(certificate.mode) modes_match = course_run_mode == certificate_mode # Grab the available date and keep it if it's the earliest one for this catalog course. if modes_match and certificate_api.is_passing_status(certificate.status): course_overview = CourseOverview.get_from_id(key) available_date = available_date_for_certificate(course_overview, certificate) earliest_course_run_date = min(filter(None, [available_date, earliest_course_run_date])) # If we're missing a cert for a course, the program isn't completed and we should just bail now if earliest_course_run_date is None: return None # Keep the catalog course date if it's the latest one program_available_date = max(filter(None, [earliest_course_run_date, program_available_date])) return program_available_date def _course_run_mode_translation(self, course_run_mode): """ Returns a canonical mode for a course run (whose data is coming from the program cache). This mode must match the certificate mode to be counted as complete. """ mappings = { # Runs of type 'credit' are counted as 'verified' since verified # certificates are earned when credit runs are completed. LEARNER-1274 # tracks a cleaner way to do this using the discovery service's # applicable_seat_types field. CourseMode.CREDIT_MODE: CourseMode.VERIFIED, } return mappings.get(course_run_mode, course_run_mode) def _certificate_mode_translation(self, certificate_mode): """ Returns a canonical mode for a certificate (whose data is coming from the database). This mode must match the course run mode to be counted as complete. """ mappings = { # Treat "no-id-professional" certificates as "professional" certificates CourseMode.NO_ID_PROFESSIONAL_MODE: CourseMode.PROFESSIONAL, } return mappings.get(certificate_mode, certificate_mode) def _is_course_complete(self, course): """Check if a user has completed a course. A course is completed if the user has earned a certificate for any of the nested course runs. Arguments: course (dict): Containing nested course runs. Returns: bool, indicating whether the course is complete. """ def reshape(course_run): """ Modify the structure of a course run dict to facilitate comparison with course run certificates. """ return { 'course_run_id': course_run['key'], # A course run's type is assumed to indicate which mode must be # completed in order for the run to count towards program completion. # This supports the same flexible program construction allowed by the # old programs service (e.g., completion of an old honor-only run may # count towards completion of a course in a program). This may change # in the future to make use of the more rigid set of "applicable seat # types" associated with each program type in the catalog. 'type': self._course_run_mode_translation(course_run['type']), } return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs']) @cached_property def completed_course_runs(self): """ Determine which course runs have been completed by the user. Returns: list of dicts, each representing a course run certificate """ return self.course_runs_with_state['completed'] @cached_property def failed_course_runs(self): """ Determine which course runs have been failed by the user. Returns: list of dicts, each a course run ID """ return [run['course_run_id'] for run in self.course_runs_with_state['failed']] @cached_property def course_runs_with_state(self): """ Determine which course runs have been completed and failed by the user. Returns: dict with a list of completed and failed runs """ course_run_certificates = certificate_api.get_certificates_for_user(self.user.username) completed_runs, failed_runs = [], [] for certificate in course_run_certificates: course_data = { 'course_run_id': unicode(certificate['course_key']), 'type': self._certificate_mode_translation(certificate['type']), } if certificate_api.is_passing_status(certificate['status']): completed_runs.append(course_data) else: failed_runs.append(course_data) return {'completed': completed_runs, 'failed': failed_runs} def _is_course_enrolled(self, course): """Check if a user is enrolled in a course. A user is considered to be enrolled in a course if they're enrolled in any of the nested course runs. Arguments: course (dict): Containing nested course runs. Returns: bool, indicating whether the course is in progress. """ return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])
class ProgramProgressMeter(object): """Utility for gauging a user's progress towards program completion. Arguments: user (User): The user for which to find programs. Keyword Arguments: enrollments (list): List of the user's enrollments. uuid (str): UUID identifying a specific program. If provided, the meter will only inspect this one program, not all programs the user may be engaged with. """ def __init__(self, site, user, enrollments=None, uuid=None): self.site = site self.user = user self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = unicode(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) self.course_grade_factory = CourseGradeFactory() if uuid: self.programs = [get_programs(self.site, uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs(self.site)) def invert_programs(self): """Intersect programs and enrollments. Builds a dictionary of program dict lists keyed by course run ID. The resulting dictionary is suitable in applications where programs must be filtered by the course runs they contain (e.g., the student dashboard). Returns: defaultdict, programs keyed by course run ID """ inverted_programs = defaultdict(list) for program in self.programs: for course in program['courses']: for course_run in course['course_runs']: course_run_id = course_run['key'] if course_run_id in self.course_run_ids: program_list = inverted_programs[course_run_id] if program not in program_list: program_list.append(program) # Sort programs by title for consistent presentation. for program_list in inverted_programs.itervalues(): program_list.sort(key=lambda p: p['title']) return inverted_programs @cached_property def engaged_programs(self): """Derive a list of programs in which the given user is engaged. Returns: list of program dicts, ordered by most recent enrollment """ inverted_programs = self.invert_programs() programs = [] # Remember that these course run ids are derived from a list of # enrollments sorted from most recent to least recent. Iterating # over the values in inverted_programs alone won't yield a program # ordering consistent with the user's enrollments. for course_run_id in self.course_run_ids: for program in inverted_programs[course_run_id]: # Dicts aren't a hashable type, so we can't use a set. Sets also # aren't ordered, which is important here. if program not in programs: programs.append(program) return programs def _is_course_in_progress(self, now, course): """Check if course qualifies as in progress as part of the program. A course is considered to be in progress if a user is enrolled in a run of the correct mode or a run of the correct mode is still available for enrollment. Arguments: now (datetime): datetime for now course (dict): Containing nested course runs. Returns: bool, indicating whether the course is in progress. """ enrolled_runs = [run for run in course['course_runs'] if run['key'] in self.course_run_ids] # Check if the user is enrolled in a required run and mode/seat. runs_with_required_mode = [ run for run in enrolled_runs if run['type'] == self.enrolled_run_modes[run['key']] ] if runs_with_required_mode: not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs] if not_failed_runs: return True # Check if seats required for course completion are still available. upgrade_deadlines = [] for run in enrolled_runs: for seat in run['seats']: if seat['type'] == run['type'] and run['type'] != self.enrolled_run_modes[run['key']]: upgrade_deadlines.append(seat['upgrade_deadline']) # An upgrade deadline of None means the course is always upgradeable. return any(not deadline or deadline and parse(deadline) > now for deadline in upgrade_deadlines) def progress(self, programs=None, count_only=True): """Gauge a user's progress towards program completion. Keyword Arguments: programs (list): Specific list of programs to check the user's progress against. If left unspecified, self.engaged_programs will be used. count_only (bool): Whether or not to return counts of completed, in progress, and unstarted courses instead of serialized representations of the courses. Returns: list of dict, each containing information about a user's progress towards completing a program. """ now = datetime.datetime.now(utc) progress = [] programs = programs or self.engaged_programs for program in programs: program_copy = deepcopy(program) completed, in_progress, not_started = [], [], [] for course in program_copy['courses']: if self._is_course_complete(course): completed.append(course) elif self._is_course_enrolled(course): course_in_progress = self._is_course_in_progress(now, course) if course_in_progress: in_progress.append(course) else: course['expired'] = not course_in_progress not_started.append(course) else: not_started.append(course) grades = {} for run in self.course_run_ids: grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run)) grades[run] = grade.percent progress.append({ 'uuid': program_copy['uuid'], 'completed': len(completed) if count_only else completed, 'in_progress': len(in_progress) if count_only else in_progress, 'not_started': len(not_started) if count_only else not_started, 'grades': grades, }) return progress @property def completed_programs(self): """Identify programs completed by the student. Returns: list of UUIDs, each identifying a completed program. """ return [program['uuid'] for program in self.programs if self._is_program_complete(program)] def _is_program_complete(self, program): """Check if a user has completed a program. A program is completed if the user has completed all nested courses. Arguments: program (dict): Representing the program whose completion to assess. Returns: bool, indicating whether the program is complete. """ return all(self._is_course_complete(course) for course in program['courses']) \ and len(program['courses']) > 0 def _is_course_complete(self, course): """Check if a user has completed a course. A course is completed if the user has earned a certificate for any of the nested course runs. Arguments: course (dict): Containing nested course runs. Returns: bool, indicating whether the course is complete. """ def reshape(course_run): """ Modify the structure of a course run dict to facilitate comparison with course run certificates. """ return { 'course_run_id': course_run['key'], # A course run's type is assumed to indicate which mode must be # completed in order for the run to count towards program completion. # This supports the same flexible program construction allowed by the # old programs service (e.g., completion of an old honor-only run may # count towards completion of a course in a program). This may change # in the future to make use of the more rigid set of "applicable seat # types" associated with each program type in the catalog. # Runs of type 'credit' are counted as 'verified' since verified # certificates are earned when credit runs are completed. LEARNER-1274 # tracks a cleaner way to do this using the discovery service's # applicable_seat_types field. 'type': 'verified' if course_run['type'] == 'credit' else course_run['type'], } return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs']) @cached_property def completed_course_runs(self): """ Determine which course runs have been completed by the user. Returns: list of dicts, each representing a course run certificate """ return self.course_runs_with_state['completed'] @cached_property def failed_course_runs(self): """ Determine which course runs have been failed by the user. Returns: list of dicts, each a course run ID """ return [run['course_run_id'] for run in self.course_runs_with_state['failed']] @cached_property def course_runs_with_state(self): """ Determine which course runs have been completed and failed by the user. Returns: dict with a list of completed and failed runs """ course_run_certificates = certificate_api.get_certificates_for_user(self.user.username) completed_runs, failed_runs = [], [] for certificate in course_run_certificates: certificate_type = certificate['type'] # Treat "no-id-professional" certificates as "professional" certificates if certificate_type == CourseMode.NO_ID_PROFESSIONAL_MODE: certificate_type = CourseMode.PROFESSIONAL course_data = { 'course_run_id': unicode(certificate['course_key']), 'type': certificate_type, } if certificate_api.is_passing_status(certificate['status']): completed_runs.append(course_data) else: failed_runs.append(course_data) return {'completed': completed_runs, 'failed': failed_runs} def _is_course_enrolled(self, course): """Check if a user is enrolled in a course. A user is considered to be enrolled in a course if they're enrolled in any of the nested course runs. Arguments: course (dict): Containing nested course runs. Returns: bool, indicating whether the course is in progress. """ return any(course_run['key'] in self.course_run_ids for course_run in course['course_runs'])