[MICROBA-437] Add api to determine demographics status based on user data
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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')),
|
||||
|
||||
|
||||
19
openedx/core/djangoapps/demographics/README.rst
Normal file
19
openedx/core/djangoapps/demographics/README.rst
Normal 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
|
||||
==================
|
||||
0
openedx/core/djangoapps/demographics/__init__.py
Normal file
0
openedx/core/djangoapps/demographics/__init__.py
Normal file
12
openedx/core/djangoapps/demographics/api/status.py
Normal file
12
openedx/core/djangoapps/demographics/api/status.py
Normal 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)
|
||||
@@ -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.
|
||||
12
openedx/core/djangoapps/demographics/rest_api/urls.py
Normal file
12
openedx/core/djangoapps/demographics/rest_api/urls.py
Normal 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))
|
||||
]
|
||||
13
openedx/core/djangoapps/demographics/rest_api/v1/urls.py
Normal file
13
openedx/core/djangoapps/demographics/rest_api/v1/urls.py
Normal 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'
|
||||
),
|
||||
]
|
||||
28
openedx/core/djangoapps/demographics/rest_api/v1/views.py
Normal file
28
openedx/core/djangoapps/demographics/rest_api/v1/views.py
Normal 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)})
|
||||
47
openedx/core/djangoapps/demographics/tests/test_status.py
Normal file
47
openedx/core/djangoapps/demographics/tests/test_status.py
Normal 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))
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user