Commit 4027dd84 by Renzo Lucioni

Use the catalog for program certificate backpopulation

This command is used to backfill missing program certificates. Formerly, it used the programs service. It now uses the catalog service exclusively.

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