Skip to content

Commit

Permalink
feat: Added management command to fill learner membership and program…
Browse files Browse the repository at this point in the history
… progress records (#7)
  • Loading branch information
zamanafzal authored Jun 12, 2022
1 parent 9a72b22 commit a3a4e05
Show file tree
Hide file tree
Showing 25 changed files with 994 additions and 13 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ Change Log
in this file. It adheres to the structure of https://keepachangelog.com/ ,
but in reStructuredText instead of Markdown (for ease of incorporation into
Sphinx documentation and the PyPI description).
This project adheres to Semantic Versioning (https://semver.org/).

.. There should always be an "Unreleased" section for changes pending release.
Unreleased
~~~~~~~~~~

[1.2.0]- 2022-06-10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Add management command and signal to update learner pathway progress and membership.

[1.1.0] - 2022-06-02
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion learner_pathway_progress/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
A plugin to track learners progress in pathways..
"""

__version__ = '1.1.0'
__version__ = '1.2.0'

default_app_config = 'learner_pathway_progress.apps.LearnerPathwayProgressConfig' # pylint: disable=invalid-name
19 changes: 18 additions & 1 deletion learner_pathway_progress/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from edx_django_utils.plugins.constants import PluginURLs
from edx_django_utils.plugins.constants import PluginSignals, PluginURLs


class LearnerPathwayProgressConfig(AppConfig):
Expand All @@ -25,4 +25,21 @@ class LearnerPathwayProgressConfig(AppConfig):
PluginURLs.RELATIVE_PATH: 'urls',
}
},
# Configuration setting for Plugin Signals for this app.
PluginSignals.CONFIG: {
'lms.djangoapp': {
PluginSignals.RECEIVERS: [
{
PluginSignals.SIGNAL_PATH: 'lms.djangoapps.grades.signals.signals'
'.COURSE_GRADE_PASSED_UPDATE_IN_LEARNER_PATHWAY',
PluginSignals.RECEIVER_FUNC_NAME: 'listen_for_course_grade_upgrade_in_learner_pathway',
},
{
PluginSignals.SENDER_PATH: 'enterprise.models.EnterpriseCourseEnrollment',
PluginSignals.SIGNAL_PATH: 'django.db.models.signals.post_save',
PluginSignals.RECEIVER_FUNC_NAME: 'create_learner_pathway_membership_for_user',
},
],
},
},
}
5 changes: 5 additions & 0 deletions learner_pathway_progress/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ class PathwayProgramStatus:
complete = 'COMPLETE'
in_progress = 'IN_PROGRESS'
not_started = 'NOT_STARTED'


CHUNK_SIZE = 1000

PATHWAY_LOGS_IDENTIFIER = 'Leaner Pathway Progress'
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Management command to update learner progress in pathway.
"""
from textwrap import dedent

from django.core.management import BaseCommand

from learner_pathway_progress.utilities import update_progress_all_pathways


class Command(BaseCommand):
"""
Command to update learner progress in pathway.
Update pathway progress for all enterprise learners who have any course enrollments.
Examples:
./manage.py lms update_all_pathways
"""
help = dedent(__doc__)

def handle(self, *args, **options):
"""
Handle the pathway progress update for enterprise learners command.
"""
update_progress_all_pathways()
Empty file.
122 changes: 122 additions & 0 deletions learner_pathway_progress/management/tests/test_update_all_pathways.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
Tests for populate pathway membership and progress management command.
"""
from unittest import TestCase
from unittest.mock import patch

import pytest
from django.contrib.sites.models import Site
from opaque_keys.edx.keys import CourseKey

from learner_pathway_progress.management.commands import update_all_pathways_progress
from learner_pathway_progress.models import LearnerPathwayMembership, LearnerPathwayProgress
from test_utils.constants import (
LEARNER_PATHWAY_UUID,
LEARNER_PATHWAY_UUID2,
LEARNER_PATHWAY_UUID3,
LEARNER_PATHWAY_UUID4,
TEST_USER_EMAIL,
LearnerPathwayProgressOutputs,
)
from test_utils.factories import (
CourseEnrollmentFactory,
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory,
UserFactory,
)


@pytest.mark.django_db
class TestUpdateLearnerPathwayProgress(TestCase):
"""
Tests upgrade progress and create membership for learner pathways management command.
"""

def setUp(self):
super().setUp()
Site.objects.get_or_create(domain='example.com')
self.command = update_all_pathways_progress.Command()
self.user = UserFactory.create(email=TEST_USER_EMAIL)
self.course_keys = []
for i in range(8):
self.course_keys.insert(i, CourseKey.from_string(f"course-v1:test-enterprise+test1+202{i}"))
for i in [0, 5]:
course_enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course_keys[i],
mode='verified'
)
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id)
EnterpriseCourseEnrollmentFactory(
enterprise_customer_user=enterprise_customer_user,
course_id=course_enrollment.course_id
)

@patch("openedx.core.djangoapps.catalog.utils.check_catalog_integration_and_get_user")
@patch("learner_pathway_progress.utilities.get_all_learner_pathways", )
def test_membership_and_progress_if_enrollment_exists(self, mocked_all_learner_pathways, mock_user):
mocked_all_learner_pathways.return_value = LearnerPathwayProgressOutputs.all_pathways_from_discovery
mock_user.return_value = self.user, None
self.command.handle()
pathway_membership = LearnerPathwayMembership.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID
).exists()
pathway_membership2 = LearnerPathwayMembership.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID2
).exists()

pathway_progress = LearnerPathwayProgress.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID
).exists()
pathway_progress2 = LearnerPathwayProgress.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID2
).exists()

self.assertTrue(pathway_membership)
self.assertTrue(pathway_membership2)
self.assertTrue(pathway_progress)
self.assertTrue(pathway_progress2)

@patch("openedx.core.djangoapps.catalog.utils.check_catalog_integration_and_get_user")
@patch("learner_pathway_progress.utilities.get_all_learner_pathways", )
def test_membership_and_progress_if_no_courses_linked_with_pathway(self, mocked_all_learner_pathways, mock_user):
mocked_all_learner_pathways.return_value = LearnerPathwayProgressOutputs.all_pathways_from_discovery
mock_user.return_value = self.user, None
self.command.handle()
pathway_membership = LearnerPathwayMembership.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID3
).exists()

pathway_progress = LearnerPathwayProgress.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID3
).exists()
self.assertFalse(pathway_membership)
self.assertFalse(pathway_progress)

@patch("openedx.core.djangoapps.catalog.utils.check_catalog_integration_and_get_user")
@patch("learner_pathway_progress.utilities.get_all_learner_pathways", )
def test_membership_and_progress_if_courses_linked_with_pathway_are_not_enrolled(
self,
mocked_all_learner_pathways,
mock_user,
):
mocked_all_learner_pathways.return_value = LearnerPathwayProgressOutputs.all_pathways_from_discovery
mock_user.return_value = self.user, None
self.command.handle()
pathway_membership = LearnerPathwayMembership.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID4
).exists()

pathway_progress = LearnerPathwayProgress.objects.filter(
user=self.user,
learner_pathway_uuid=LEARNER_PATHWAY_UUID4
).exists()
self.assertFalse(pathway_membership)
self.assertFalse(pathway_progress)
4 changes: 2 additions & 2 deletions learner_pathway_progress/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def get_learner_course_status(user, course):
"""
Get the progress of the learner in the course.
"""
course_runs = course['course_runs'] or []
course_runs = course.get('course_runs') or []
learner_enrollments = []
for course_run in course_runs:
learner_course_grade = PersistentCourseGrade.objects.filter(
Expand Down Expand Up @@ -101,7 +101,7 @@ def update_pathway_progress(self):
Update the progress for the learner in the pathway.
"""
pathway_snapshot = json.loads(self.learner_pathway_progress)
pathway_steps = pathway_snapshot['steps'] or []
pathway_steps = pathway_snapshot.get('steps') or []
for step in pathway_steps:
step_courses = step['courses'] or []
step_programs = step['programs'] or []
Expand Down
62 changes: 62 additions & 0 deletions learner_pathway_progress/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Module for learner_pathway_progress related signals.
"""
from logging import getLogger

from django.contrib.auth import get_user_model
from django.db.models.signals import post_save

from learner_pathway_progress.models import LearnerPathwayMembership
from learner_pathway_progress.utilities import (
get_learner_pathways_associated_with_course,
update_learner_pathway_progress,
)

try:
from enterprise.models import EnterpriseCourseEnrollment
except ImportError:
EnterpriseCourseEnrollment = None

log = getLogger(__name__)

User = get_user_model()


def listen_for_course_grade_upgrade_in_learner_pathway(sender, user_id, course_id,
**kwargs): # pylint: disable=unused-argument
"""
Listen for a signal indicating that the user has passed course grade.
Call update_learner_pathway_progress function to update learner course grade in pathway.
"""
update_learner_pathway_progress(user_id, course_id)


def create_learner_pathway_membership_for_user(sender, instance, created, **kwargs): # pylint: disable=unused-argument
"""
Watches for post_save signal for creates on the EnterpriseCourseEnrollment table.
Generate a Learner Pathway Membership for the User
"""
if created:
user_id = instance.enterprise_customer_user.user_id
user = User.objects.filter(id=user_id).first()

learner_pathways = get_learner_pathways_associated_with_course(user, instance.course_id)
if learner_pathways:
for pathway in learner_pathways:
_, created = LearnerPathwayMembership.objects.get_or_create(
user=user,
learner_pathway_uuid=pathway
)
membership_status = 'updated'
if created:
membership_status = 'created'
log.info(
f"Membership for pathway:{pathway} {membership_status} for user:{user.email}"
f" with course: {instance.course_id}"
)


if EnterpriseCourseEnrollment is not None:
post_save.connect(create_learner_pathway_membership_for_user, sender=EnterpriseCourseEnrollment)
3 changes: 0 additions & 3 deletions learner_pathway_progress/signals/handlers.py

This file was deleted.

3 changes: 0 additions & 3 deletions learner_pathway_progress/signals/signals.py

This file was deleted.

Loading

0 comments on commit a3a4e05

Please sign in to comment.