Commit 8a53f3b6 by Renzo Lucioni Committed by GitHub

Merge pull request #14430 from edx/renzo/backpopulation-from-catalog

Use the catalog for program certificate backpopulation
parents 255af25a 4027dd84
......@@ -27,7 +27,7 @@ class CatalogIntegration(ConfigurationModel):
service_username = models.CharField(
max_length=100,
default="lms_catalog_service_user",
default='lms_catalog_service_user',
null=False,
blank=False,
help_text=_(
......
......@@ -3,21 +3,18 @@ from collections import namedtuple
import logging
from django.contrib.auth.models import User
from django.core.management import BaseCommand, CommandError
from django.core.management import BaseCommand
from django.db.models import Q
from opaque_keys.edx.keys import CourseKey
from provider.oauth2.models import Client
import waffle
from certificates.models import GeneratedCertificate, CertificateStatuses # pylint: disable=import-error
from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates
from openedx.core.djangoapps.programs.utils import get_programs
# TODO: Log to console, even with debug mode disabled?
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
RunMode = namedtuple('RunMode', ['course_key', 'mode_slug'])
CourseRun = namedtuple('CourseRun', ['key', 'type'])
class Command(BaseCommand):
......@@ -27,8 +24,7 @@ class Command(BaseCommand):
Celery task for further (parallelized) processing.
"""
help = 'Backpopulate missing program credentials.'
client = None
run_modes = None
course_runs = None
usernames = None
def add_arguments(self, parser):
......@@ -41,20 +37,10 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
catalog_config = CatalogIntegration.current()
try:
user = User.objects.get(username=catalog_config.service_username)
except:
raise CommandError(
'User with username [{}] not found. '
'A service user is required to run this command.'.format(catalog_config.service_username)
)
self._load_run_modes(user)
logger.info('Loading programs from the catalog.')
self._load_course_runs()
logger.info('Looking for users who may be eligible for a program certificate.')
self._load_usernames()
if options.get('commit'):
......@@ -84,38 +70,44 @@ class Command(BaseCommand):
failed
)
def _load_run_modes(self, user):
"""Find all run modes which are part of a program."""
use_catalog = waffle.switch_is_active('get_programs_from_catalog')
programs = get_programs(user, use_catalog=use_catalog)
self.run_modes = self._flatten(programs)
def _load_course_runs(self):
"""Find all course runs which are part of a program."""
programs = get_programs()
self.course_runs = self._flatten(programs)
def _flatten(self, programs):
"""Flatten program dicts into a set of run modes."""
run_modes = set()
"""Flatten programs into a set of course runs."""
course_runs = set()
for program in programs:
for course_code in program['course_codes']:
for run in course_code['run_modes']:
course_key = CourseKey.from_string(run['course_key'])
run_modes.add(
RunMode(course_key, run['mode_slug'])
for course in program['courses']:
for run in course['course_runs']:
key = CourseKey.from_string(run['key'])
course_runs.add(
CourseRun(key, run['type'])
)
return run_modes
return course_runs
def _load_usernames(self):
"""Identify a subset of users who may be eligible for a program certificate.
This is done by finding users who have earned a certificate in at least one
program course code's run mode.
This is done by finding users who have earned a qualifying certificate in
at least one program course's course run.
"""
status_query = Q(status__in=CertificateStatuses.PASSED_STATUSES)
run_mode_query = reduce(
course_run_query = reduce(
lambda x, y: x | y,
[Q(course_id=r.course_key, mode=r.mode_slug) for r in self.run_modes]
# 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.
[Q(course_id=run.key, mode=run.type) for run in self.course_runs]
)
query = status_query & run_mode_query
query = status_query & course_run_query
username_dicts = GeneratedCertificate.eligible_certificates.filter(query).values('user__username').distinct()
self.usernames = [d['user__username'] for d in username_dicts]
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