Commit 1a7e1a31 by Renzo Lucioni

Merge pull request #12700 from edx/renzo/local-program-completion

Use cached data to find completed programs on LMS
parents 9ba70484 6d7938fd
...@@ -56,8 +56,6 @@ class TestProgramListing( ...@@ -56,8 +56,6 @@ class TestProgramListing(
def _create_course_and_enroll(self, student, org, course, run): def _create_course_and_enroll(self, student, org, course, run):
""" """
Creates a course and associated enrollment. Creates a course and associated enrollment.
TODO: Use CourseEnrollmentFactory to avoid course creation.
""" """
course_location = locator.CourseLocator(org, course, run) course_location = locator.CourseLocator(org, course, run)
course = CourseFactory.create( course = CourseFactory.create(
......
...@@ -11,7 +11,6 @@ from edxmako.shortcuts import render_to_response ...@@ -11,7 +11,6 @@ from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.credentials.utils import get_programs_credentials from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils from openedx.core.djangoapps.programs import utils
from student.views import get_course_enrollments
@login_required @login_required
...@@ -22,8 +21,7 @@ def view_programs(request): ...@@ -22,8 +21,7 @@ def view_programs(request):
if not show_program_listing: if not show_program_listing:
raise Http404 raise Http404
enrollments = list(get_course_enrollments(request.user, None, [])) meter = utils.ProgramProgressMeter(request.user)
meter = utils.ProgramProgressMeter(request.user, enrollments)
programs = meter.engaged_programs programs = meter.engaged_programs
# TODO: Pull 'xseries' string from configuration model. # TODO: Pull 'xseries' string from configuration model.
......
...@@ -10,7 +10,7 @@ from edx_rest_api_client.client import EdxRestApiClient ...@@ -10,7 +10,7 @@ from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.utils import get_user_credentials from openedx.core.djangoapps.credentials.utils import get_user_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import get_completed_courses from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.lib.token_utils import get_id_token from openedx.core.lib.token_utils import get_id_token
...@@ -35,21 +35,20 @@ def get_api_client(api_config, student): ...@@ -35,21 +35,20 @@ def get_api_client(api_config, student):
return EdxRestApiClient(api_config.internal_api_url, jwt=id_token) return EdxRestApiClient(api_config.internal_api_url, jwt=id_token)
def get_completed_programs(client, course_certificates): def get_completed_programs(student):
""" """
Given a set of completed courses, determine which programs are completed. Given a set of completed courses, determine which programs are completed.
Args: Args:
client: student (User): Representing the student whose completed programs to check for.
programs API client (EdxRestApiClient)
course_certificates:
iterable of dicts with structure {'course_id': course_key, 'mode': cert_type}
Returns: Returns:
list of program ids list of program ids
""" """
return client.programs.complete.post({'completed_courses': course_certificates})['program_ids'] meter = ProgramProgressMeter(student)
return meter.completed_programs
def get_awarded_certificate_programs(student): def get_awarded_certificate_programs(student):
...@@ -147,29 +146,9 @@ def award_program_certificates(self, username): ...@@ -147,29 +146,9 @@ def award_program_certificates(self, username):
# Don't retry for this case - just conclude the task. # Don't retry for this case - just conclude the task.
return return
# Fetch the set of all course runs for which the user has earned a program_ids = get_completed_programs(student)
# certificate.
course_certs = get_completed_courses(student)
if not course_certs:
# Highly unlikely, since at present the only trigger for this task
# is the earning of a new course certificate. However, it could be
# that the transaction in which a course certificate was awarded
# was subsequently rolled back, which could lead to an empty result
# here, so we'll at least log that this happened before exiting.
#
# If this task is ever updated to support revocation of program
# certs, this branch should be removed, since it could make sense
# in that case to call this task for a user without any (valid)
# course certs.
LOGGER.warning('Task award_program_certificates was called for user %s with no completed courses', username)
return
# Invoke the Programs API completion check endpoint to identify any
# programs that are satisfied by these course completions.
programs_client = get_api_client(config, student)
program_ids = get_completed_programs(programs_client, course_certs)
if not program_ids: if not program_ids:
# Again, no reason to continue beyond this point unless/until this # No reason to continue beyond this point unless/until this
# task gets updated to support revocation of program certs. # task gets updated to support revocation of program certs.
LOGGER.info('Task award_program_certificates was called for user %s with no completed programs', username) LOGGER.info('Task award_program_certificates was called for user %s with no completed programs', username)
return return
......
...@@ -5,6 +5,7 @@ import logging ...@@ -5,6 +5,7 @@ import logging
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
import pytz import pytz
...@@ -170,29 +171,27 @@ class ProgramProgressMeter(object): ...@@ -170,29 +171,27 @@ class ProgramProgressMeter(object):
Arguments: Arguments:
user (User): The user for which to find programs. user (User): The user for which to find programs.
enrollments (list): The user's active enrollments.
""" """
def __init__(self, user, enrollments): def __init__(self, user):
self.user = user self.user = user
self.course_ids = None
enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True) self.programs = get_programs(self.user)
# enrollment.course_id is really a course key ಠ_ಠ self.course_certs = get_completed_courses(self.user)
self.course_ids = [unicode(e.course_id) for e in enrollments]
self.engaged_programs = self._find_engaged_programs(self.user)
self.course_certs = None
def _find_engaged_programs(self, user): @cached_property
def engaged_programs(self):
"""Derive a list of programs in which the given user is engaged. """Derive a list of programs in which the given user is engaged.
Arguments:
user (User): The user for which to find engaged programs.
Returns: Returns:
list of program dicts, ordered by most recent enrollment. list of program dicts, ordered by most recent enrollment.
""" """
programs = get_programs(user) enrollments = CourseEnrollment.enrollments_for_user(self.user)
flattened = flatten_programs(programs, self.course_ids) enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True)
# enrollment.course_id is really a course key ಠ_ಠ
self.course_ids = [unicode(e.course_id) for e in enrollments]
flattened = flatten_programs(self.programs, self.course_ids)
engaged_programs = [] engaged_programs = []
for course_id in self.course_ids: for course_id in self.course_ids:
...@@ -210,8 +209,6 @@ class ProgramProgressMeter(object): ...@@ -210,8 +209,6 @@ class ProgramProgressMeter(object):
list of dict, each containing information about a user's progress list of dict, each containing information about a user's progress
towards completing a program. towards completing a program.
""" """
self.course_certs = get_completed_courses(self.user)
progress = [] progress = []
for program in self.engaged_programs: for program in self.engaged_programs:
completed, in_progress, not_started = [], [], [] completed, in_progress, not_started = [], [], []
...@@ -219,9 +216,9 @@ class ProgramProgressMeter(object): ...@@ -219,9 +216,9 @@ class ProgramProgressMeter(object):
for course_code in program['course_codes']: for course_code in program['course_codes']:
name = course_code['display_name'] name = course_code['display_name']
if self._is_complete(course_code): if self._is_course_code_complete(course_code):
completed.append(name) completed.append(name)
elif self._is_in_progress(course_code): elif self._is_course_code_in_progress(course_code):
in_progress.append(name) in_progress.append(name)
else: else:
not_started.append(name) not_started.append(name)
...@@ -235,11 +232,33 @@ class ProgramProgressMeter(object): ...@@ -235,11 +232,33 @@ class ProgramProgressMeter(object):
return progress return progress
def _is_complete(self, course_code): @property
def completed_programs(self):
"""Identify programs completed by the student.
Returns:
list of int, each the ID of a completed program.
"""
return [program['id'] 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 course codes.
Arguments:
program (dict): Representing the program whose completion to assess.
Returns:
bool, whether the program is complete.
"""
return all(self._is_course_code_complete(course_code) for course_code in program['course_codes'])
def _is_course_code_complete(self, course_code):
"""Check if a user has completed a course code. """Check if a user has completed a course code.
A course code qualifies as completed if the user has earned a A course code is completed if the user has earned a certificate
certificate in the right mode for any nested run. in the right mode for any nested run.
Arguments: Arguments:
course_code (dict): Containing nested run modes. course_code (dict): Containing nested run modes.
...@@ -247,12 +266,9 @@ class ProgramProgressMeter(object): ...@@ -247,12 +266,9 @@ class ProgramProgressMeter(object):
Returns: Returns:
bool, whether the course code is complete. bool, whether the course code is complete.
""" """
return any([ return any(self._parse(run_mode) in self.course_certs for run_mode in course_code['run_modes'])
self._parse(run_mode) in self.course_certs
for run_mode in course_code['run_modes']
])
def _is_in_progress(self, course_code): def _is_course_code_in_progress(self, course_code):
"""Check if a user is in the process of completing a course code. """Check if a user is in the process of completing a course code.
A user is in the process of completing a course code if they're A user is in the process of completing a course code if they're
...@@ -264,10 +280,7 @@ class ProgramProgressMeter(object): ...@@ -264,10 +280,7 @@ class ProgramProgressMeter(object):
Returns: Returns:
bool, whether the course code is in progress. bool, whether the course code is in progress.
""" """
return any([ return any(run_mode['course_key'] in self.course_ids for run_mode in course_code['run_modes'])
run_mode['course_key'] in self.course_ids
for run_mode in course_code['run_modes']
])
def _parse(self, run_mode): def _parse(self, run_mode):
"""Modify the structure of a run mode dict. """Modify the structure of a run mode dict.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment