Add notification to program detail view
ECOM-7385
This commit is contained in:
@@ -40,6 +40,7 @@ CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
|
||||
ECOMMERCE_URL_ROOT = 'https://example-ecommerce.com'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@attr(shard=2)
|
||||
@skip_unless_lms
|
||||
@mock.patch(UTILS_MODULE + '.get_programs')
|
||||
@@ -53,7 +54,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
def _create_enrollments(self, *course_run_ids):
|
||||
"""Variadic helper used to create course run enrollments."""
|
||||
for course_run_id in course_run_ids:
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_id)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_id, mode='verified')
|
||||
|
||||
def _assert_progress(self, meter, *progresses):
|
||||
"""Variadic helper used to verify progress calculations."""
|
||||
@@ -150,6 +151,46 @@ class TestProgramProgressMeter(TestCase):
|
||||
|
||||
self.assertEqual(meter.progress(count_only=False), expected)
|
||||
|
||||
@ddt.data(1, -1)
|
||||
def test_in_progress_course_upgrade_deadline_check(self, modifier, mock_get_programs):
|
||||
"""
|
||||
Verify that if the user's enrollment is not of the same type as the course run,
|
||||
the course will only count as in progress if there is another available seat with
|
||||
the right type, where the upgrade deadline has not expired.
|
||||
"""
|
||||
course_run_key = generate_course_run_key()
|
||||
now = datetime.datetime.now(utc)
|
||||
date_modifier = modifier * datetime.timedelta(days=1)
|
||||
seat_with_upgrade_deadline = SeatFactory(type='test', upgrade_deadline=str(now + date_modifier))
|
||||
enrolled_seat = SeatFactory(type='verified')
|
||||
seats = [seat_with_upgrade_deadline, enrolled_seat]
|
||||
|
||||
data = [
|
||||
ProgramFactory(
|
||||
courses=[
|
||||
CourseFactory(course_runs=[
|
||||
CourseRunFactory(key=course_run_key, type='test', seats=seats),
|
||||
]),
|
||||
]
|
||||
)
|
||||
]
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
self._create_enrollments(course_run_key)
|
||||
|
||||
meter = ProgramProgressMeter(self.user)
|
||||
|
||||
program = data[0]
|
||||
expected = [
|
||||
ProgressFactory(
|
||||
uuid=program['uuid'],
|
||||
completed=0,
|
||||
in_progress=1 if modifier == 1 else 0,
|
||||
not_started=1 if modifier == -1 else 0
|
||||
)
|
||||
]
|
||||
self.assertEqual(meter.progress(count_only=True), expected)
|
||||
|
||||
def test_mutiple_program_engagement(self, mock_get_programs):
|
||||
"""
|
||||
Verify that correct programs are returned in the correct order when the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Helper functions for working with Programs."""
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
import datetime
|
||||
from urlparse import urljoin
|
||||
|
||||
@@ -69,8 +70,14 @@ class ProgramProgressMeter(object):
|
||||
self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user))
|
||||
self.enrollments.sort(key=lambda e: e.created, reverse=True)
|
||||
|
||||
# enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻
|
||||
self.course_run_ids = [unicode(e.course_id) for e in self.enrollments]
|
||||
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)
|
||||
self.enrolled_run_modes[enrollment_id] = enrollment.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)
|
||||
|
||||
if uuid:
|
||||
self.programs = [get_programs(uuid=uuid)]
|
||||
@@ -127,6 +134,44 @@ class ProgramProgressMeter(object):
|
||||
|
||||
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.
|
||||
"""
|
||||
# Part 1: Check if any of the seats you are enrolled in qualify this course as 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 the required mode for the run
|
||||
runs_with_required_mode = [
|
||||
run for run in enrolled_runs
|
||||
if run['type'] == self.enrolled_run_modes[run['key']]
|
||||
]
|
||||
if runs_with_required_mode:
|
||||
# Check if the runs you are enrolled in with the right mode are not failed
|
||||
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
|
||||
# Part 2: Check if any of the seats you are not enrolled in
|
||||
# in the runs you are enrolled in qualify this course as in progress
|
||||
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'])
|
||||
|
||||
course_still_upgradeable = any(
|
||||
(deadline is not None) and (parse(deadline) > now) for deadline in upgrade_deadlines
|
||||
)
|
||||
return course_still_upgradeable
|
||||
|
||||
def progress(self, programs=None, count_only=True):
|
||||
"""Gauge a user's progress towards program completion.
|
||||
|
||||
@@ -142,21 +187,29 @@ class ProgramProgressMeter(object):
|
||||
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['courses']:
|
||||
for course in program_copy['courses']:
|
||||
if self._is_course_complete(course):
|
||||
completed.append(course)
|
||||
elif self._is_course_in_progress(course):
|
||||
in_progress.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)
|
||||
|
||||
progress.append({
|
||||
'uuid': program['uuid'],
|
||||
'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,
|
||||
@@ -226,17 +279,43 @@ class ProgramProgressMeter(object):
|
||||
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)
|
||||
return [
|
||||
{'course_run_id': unicode(certificate['course_key']), 'type': certificate['type']}
|
||||
for certificate in course_run_certificates
|
||||
if certificate_api.is_passing_status(certificate['status'])
|
||||
]
|
||||
completed_runs, failed_runs = [], []
|
||||
for certificate in course_run_certificates:
|
||||
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_in_progress(self, course):
|
||||
"""Check if a user is in the process of completing a course.
|
||||
def _is_course_enrolled(self, course):
|
||||
"""Check if a user is enrolled in a course.
|
||||
|
||||
A user is considered to be in the process of completing a course if
|
||||
A user is considered to be enrolled in a course if
|
||||
they're enrolled in any of the nested course runs.
|
||||
|
||||
Arguments:
|
||||
|
||||
Reference in New Issue
Block a user