[MICROBA-437] Add api to determine demographics status based on user data

This commit is contained in:
Albert (AJ) St. Aubin
2020-07-09 15:06:21 -04:00
parent 887941f3ce
commit 77f3d9099b
16 changed files with 282 additions and 4 deletions

View File

@@ -2559,6 +2559,9 @@ INSTALLED_APPS = [
# Management of external user ids
'openedx.core.djangoapps.external_user_ids',
# Provides api for Demographics support
'openedx.core.djangoapps.demographics',
# Management of per-user schedules
'openedx.core.djangoapps.schedules',
'rest_framework_jwt',

View File

@@ -120,6 +120,9 @@ urlpatterns = [
url(r'^api/entitlements/', include(('entitlements.rest_api.urls', 'common.djangoapps.entitlements'),
namespace='entitlements_api')),
# Demographics API RESTful endpoints
url(r'^api/demographics/', include('openedx.core.djangoapps.demographics.rest_api.urls')),
# Courseware search endpoints
url(r'^search/', include('search.urls')),

View File

@@ -0,0 +1,19 @@
Status: Active
Responsibilities
================
The Demographics app is an application to support the Demographics feature set
and IDA. It serves as the access point for demographics related status.
Direction: Decompose
===============
This app may be removed in the future as the Demographics feature set expands
to a larger set of users. It is not recommended that new features are added
here at this time.
Glossary
========
IDA: Independently Deployable Application
More Documentation
==================

View File

@@ -0,0 +1,12 @@
"""
Python API for Demographics Status
"""
from openedx.features.enterprise_support.utils import is_enterprise_learner
from openedx.core.djangoapps.programs.utils import is_user_enrolled_in_program_type
def show_user_demographics(user):
# Is the learner enrolled in MicroBachelors Program or is the learner an Enterprise learner?
is_user_in_microbachelors_program = is_user_enrolled_in_program_type(user, "microbachelors")
return is_user_in_microbachelors_program and not is_enterprise_learner(user)

View File

@@ -0,0 +1,22 @@
Django Application to Support Demographics Features
---------------------------------------------------
Status
======
Accepted
Context
=======
To support demographics features and the IDA we need to be able to access the
current state of a User from the LMS (i.e. Program Enrollments, Enterprise status).
Decisions
=========
* To meet this need we are creating the Demographics Django Application in the
Open edX core. This application will contain utilities and APIs that will support
the Demographics feature set until they are replaced with other more general APIs
or no longer needed.

View File

@@ -0,0 +1,12 @@
"""
Demographics API URLs.
"""
from django.conf.urls import include, url
from .v1 import urls as v1_urls
app_name = 'openedx.core.djangoapps.demographics'
urlpatterns = [
url(r'^v1/', include(v1_urls))
]

View File

@@ -0,0 +1,13 @@
"""
URL Routes for this app.
"""
from django.conf.urls import url
from .views import DemographicsStatusView
urlpatterns = [
url(
r'^demographics/status/$',
DemographicsStatusView.as_view(),
name='demographics_status'
),
]

View File

@@ -0,0 +1,28 @@
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework.views import APIView
from rest_framework import permissions
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from openedx.core.djangoapps.demographics.api.status import show_user_demographics
class DemographicsStatusView(APIView):
"""
Demographics display status for the User.
The API will return whether or not to display the Demographics UI based on
the User's status in the Platform
"""
authentication_classes = (JwtAuthentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated, )
def get(self, request):
"""
GET /api/user/v1/accounts/demographics_status
This is a Web API to determine whether or not we should show Demographics to a learner
based on their enrollment status.
"""
user = request.user
return Response({'display': show_user_demographics(user)})

View File

@@ -0,0 +1,47 @@
"""
Test status utilities
"""
import mock
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from opaque_keys.edx.keys import CourseKey
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangoapps.catalog.tests.factories import (
ProgramFactory,
)
from openedx.core.djangoapps.demographics.api.status import show_user_demographics
from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerUserFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
MICROBACHELORS = 'microbachelors'
@skip_unless_lms
@mock.patch('openedx.core.djangoapps.programs.utils.get_programs_by_type')
class TestShowDemographics(SharedModuleStoreTestCase):
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.store = modulestore()
cls.user = UserFactory()
cls.program = ProgramFactory(type=MICROBACHELORS)
cls.catalog_course_run = cls.program['courses'][0]['course_runs'][0]
cls.course_key = CourseKey.from_string(cls.catalog_course_run['key'])
cls.course_run = CourseFactory.create(
org=cls.course_key.org,
number=cls.course_key.course,
run=cls.course_key.run,
modulestore=cls.store,
)
CourseModeFactory.create(course_id=cls.course_run.id, mode_slug=CourseMode.VERIFIED)
def test_user_enterprise(self, mock_get_programs_by_type):
mock_get_programs_by_type.return_value = [self.program]
EnterpriseCustomerUserFactory.create(user_id=self.user.id)
self.assertFalse(show_user_demographics(user=self.user))

View File

@@ -20,12 +20,13 @@ from testfixtures import LogCapture
from waffle.testutils import override_switch
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from entitlements.tests.factories import CourseEntitlementFactory
from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory,
CourseRunFactory,
@@ -42,13 +43,17 @@ from openedx.core.djangoapps.programs.utils import (
ProgramMarketingDataExtender,
ProgramProgressMeter,
get_certificates,
get_logged_in_program_certificate_url
get_logged_in_program_certificate_url,
is_user_enrolled_in_program_type
)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory, UserFactory
from util.date_utils import strftime_localized
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE
)
from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory
CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
@@ -1551,3 +1556,61 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
data['skus'],
[course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']]
)
@skip_unless_lms
@mock.patch('openedx.core.djangoapps.programs.utils.get_programs_by_type')
class TestProgramEnrollment(SharedModuleStoreTestCase):
"""
Tests to test program enrollment utility methods for program data from the program cache.
Requests to the data in the Program cache are mocked out.
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
MICROBACHELORS = 'microbachelors'
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.store = modulestore()
cls.user = UserFactory()
cls.program = ProgramFactory(type=cls.MICROBACHELORS)
cls.catalog_course_run = cls.program['courses'][0]['course_runs'][0]
cls.course_key = CourseKey.from_string(cls.catalog_course_run['key'])
cls.course_run = ModuleStoreCourseFactory.create(
org=cls.course_key.org,
number=cls.course_key.course,
run=cls.course_key.run,
modulestore=cls.store,
)
CourseModeFactory.create(course_id=cls.course_run.id, mode_slug=CourseMode.VERIFIED)
def test_user_not_in_program(self, mock_get_programs_by_type):
mock_get_programs_by_type.return_value = [self.program]
self.assertFalse(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
def test_user_enrolled_in_mb_program(self, mock_get_programs_by_type):
CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.VERIFIED)
mock_get_programs_by_type.return_value = [self.program]
self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
def test_user_enrolled_unpaid_in_program(self, mock_get_programs_by_type):
CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.AUDIT)
mock_get_programs_by_type.return_value = [self.program]
self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
def test_user_enrolled_unpaid_in_program_paid_only_request(self, mock_get_programs_by_type):
CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.AUDIT)
mock_get_programs_by_type.return_value = [self.program]
self.assertFalse(
is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS, paid_modes=True)
)
def test_user_with_entitlement_no_enrollment(self, mock_get_programs_by_type):
CourseEntitlementFactory.create(
user=self.user,
mode=CourseMode.VERIFIED,
course_uuid=self.program['courses'][0]['uuid']
)
mock_get_programs_by_type.return_value = [self.program]
self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))

View File

@@ -12,6 +12,7 @@ import six
from dateutil.parser import parse
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.urls import reverse
from django.utils.functional import cached_property
@@ -22,15 +23,21 @@ from requests.exceptions import ConnectionError, Timeout
from six.moves.urllib.parse import urljoin, urlparse, urlunparse # pylint: disable=import-error
from course_modes.models import CourseMode
from entitlements.api import get_active_entitlement_list_for_user
from entitlements.models import CourseEntitlement
from lms.djangoapps.certificates import api as certificate_api
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.commerce.utils import EcommerceService
from openedx.core.djangoapps.catalog.utils import get_fulfillable_course_runs_for_entitlement, get_programs
from openedx.core.djangoapps.catalog.utils import (
get_fulfillable_course_runs_for_entitlement,
get_programs,
get_programs_by_type
)
from openedx.core.djangoapps.certificates.api import available_date_for_certificate
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.utils import get_credentials
from openedx.core.djangoapps.enrollments.api import get_enrollments
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -892,3 +899,51 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
for instructor in course_instructors.get('instructors', []):
if instructor.get('name', '').strip() not in curr_instructors_names:
self.instructors.append(instructor)
def is_user_enrolled_in_program_type(user, program_type, paid_modes=False):
"""
This method will Look at the learners Enrollments and Entitlements to determine
if a learner is enrolled in a Program of the given type.
NOTE: This method relies on the Program Cache right now. The goal is to move away from this
in the future.
Arguments:
user (User): The user we are looking for.
program_type (String): The Program type we are looking for.
paid_modes (bool): Request if the user is enrolled in a Program in a paid mode, False by default.
Returns:
bool: True is the user is enrolled in programs of the requested Type
"""
course_runs = set()
course_uuids = set()
programs = get_programs_by_type(Site.objects.get_current(), program_type)
if not programs:
return False
for program in programs:
for course in program.get('courses', []):
course_uuids.add(course.get('uuid'))
for course_run in course.get('course_runs', []):
course_runs.add(course_run['key'])
# Check Entitlements first, because there will be less Course Entitlements than
# Course Run Enrollments.
student_entitlements = get_active_entitlement_list_for_user(user)
for entitlement in student_entitlements:
if str(entitlement.course_uuid) in course_uuids:
return True
student_enrollments = get_enrollments(user.username)
for enrollment in student_enrollments:
course_run_id = enrollment['course_details']['course_id']
if paid_modes:
course_run_key = CourseKey.from_string(course_run_id)
paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_key)]
if enrollment['mode'] in paid_modes and course_run_id in course_runs:
return True
elif course_run_id in course_runs:
return True
return False

View File

@@ -88,6 +88,7 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.schedules.apps.SchedulesConfig',
'openedx.core.djangoapps.theming.apps.ThemingConfig',
'openedx.core.djangoapps.external_user_ids',
'openedx.core.djangoapps.demographics',
'experiments',
'openedx.features.content_type_gating',