@@ -3,16 +3,11 @@
|
||||
import uuid
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
|
||||
from openedx.core.djangoapps.credentials.tests import factories
|
||||
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
|
||||
from openedx.core.djangoapps.credentials.utils import (
|
||||
get_courses_completion_status,
|
||||
get_credentials,
|
||||
get_credentials_records_url,
|
||||
)
|
||||
@@ -107,33 +102,3 @@ class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase):
|
||||
|
||||
result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456")
|
||||
assert result == "https://credentials.example.com/records/programs/abcdefghijklmnopqrstuvwxyz123456"
|
||||
|
||||
@mock.patch("requests.Response.raise_for_status")
|
||||
@mock.patch("requests.Response.json")
|
||||
@mock.patch(UTILS_MODULE + ".get_credentials_api_client")
|
||||
def test_get_courses_completion_status(self, mock_get_api_client, mock_json, mock_raise):
|
||||
"""
|
||||
Test to verify the functionality of get_courses_completion_status
|
||||
"""
|
||||
UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME)
|
||||
course_statuses = factories.UserCredentialsCourseRunStatus.create_batch(3)
|
||||
response_data = [course_status["course_run"]["key"] for course_status in course_statuses]
|
||||
mock_raise.return_value = None
|
||||
mock_json.return_value = {
|
||||
"lms_user_id": self.user.id,
|
||||
"status": course_statuses,
|
||||
"username": self.user.username,
|
||||
}
|
||||
mock_get_api_client.return_value.post.return_value = Response()
|
||||
course_run_keys = [course_status["course_run"]["key"] for course_status in course_statuses]
|
||||
api_response, is_exception = get_courses_completion_status(self.user.id, course_run_keys)
|
||||
assert api_response == response_data
|
||||
assert is_exception is False
|
||||
|
||||
@mock.patch("requests.Response.raise_for_status")
|
||||
def test_get_courses_completion_status_api_error(self, mock_raise):
|
||||
mock_raise.return_value = HTTPError("An Error occured")
|
||||
UserFactory.create(username=settings.CREDENTIALS_SERVICE_USERNAME)
|
||||
api_response, is_exception = get_courses_completion_status(self.user.id, ["fake1", "fake2", "fake3"])
|
||||
assert api_response == []
|
||||
assert is_exception is True
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Dict, List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from edx_rest_api_client.auth import SuppliedJwtAuth
|
||||
|
||||
@@ -121,59 +120,3 @@ def get_credentials(
|
||||
cache_key=cache_key,
|
||||
raise_on_error=raise_on_error,
|
||||
)
|
||||
|
||||
|
||||
def get_courses_completion_status(username, course_run_ids):
|
||||
"""
|
||||
Given the username and course run ids, checks for course completion status
|
||||
Arguments:
|
||||
username (User): Username of the user whose credentials are being requested.
|
||||
course_run_ids(List): list of course run ids for which we need to check the completion status
|
||||
Returns:
|
||||
list of course_run_ids for which user has completed the course
|
||||
Boolean: True if an exception occurred while calling the api, False otherwise
|
||||
"""
|
||||
credential_configuration = CredentialsApiConfig.current()
|
||||
if not credential_configuration.enabled:
|
||||
log.warning("%s configuration is disabled.", credential_configuration.API_NAME)
|
||||
return [], False
|
||||
|
||||
completion_status_url = f"{settings.CREDENTIALS_INTERNAL_SERVICE_URL}/api" "/credentials/v1/learner_cert_status/"
|
||||
try:
|
||||
api_client = get_credentials_api_client(User.objects.get(username=settings.CREDENTIALS_SERVICE_USERNAME))
|
||||
api_response = api_client.post(
|
||||
completion_status_url,
|
||||
json={
|
||||
"username": username,
|
||||
"course_runs": course_run_ids,
|
||||
},
|
||||
)
|
||||
api_response.raise_for_status()
|
||||
course_completion_response = api_response.json()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
log.exception(
|
||||
"An unexpected error occurred while reqeusting course completion statuses "
|
||||
"for user [%s] for course_run_ids [%s] with exc [%s]:",
|
||||
username,
|
||||
course_run_ids,
|
||||
exc,
|
||||
)
|
||||
return [], True
|
||||
log.info(
|
||||
"Course completion status response for user [%s] for course_run_ids [%s] is [%s]",
|
||||
username,
|
||||
course_run_ids,
|
||||
course_completion_response,
|
||||
)
|
||||
# Yes, This is course_credentials_data. The key is named status but
|
||||
# it contains all the courses data from credentials.
|
||||
course_credentials_data = course_completion_response.get("status", [])
|
||||
if course_credentials_data is not None:
|
||||
filtered_records = [
|
||||
course_data["course_run"]["key"]
|
||||
for course_data in course_credentials_data
|
||||
if course_data["course_run"]["key"] in course_run_ids
|
||||
and course_data["status"] == settings.CREDENTIALS_COURSE_COMPLETION_STATE
|
||||
]
|
||||
return filtered_records, False
|
||||
return [], False
|
||||
|
||||
@@ -6,7 +6,6 @@ import uuid
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from unittest import mock
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
@@ -44,10 +43,8 @@ from openedx.core.djangoapps.programs.utils import (
|
||||
ProgramDataExtender,
|
||||
ProgramMarketingDataExtender,
|
||||
ProgramProgressMeter,
|
||||
get_buy_subscription_url,
|
||||
get_certificates,
|
||||
get_logged_in_program_certificate_url,
|
||||
get_programs_subscription_data,
|
||||
is_user_enrolled_in_program_type
|
||||
)
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
@@ -1759,100 +1756,3 @@ class TestProgramEnrollment(SharedModuleStoreTestCase):
|
||||
)
|
||||
mock_get_programs_by_type.return_value = [self.program]
|
||||
assert is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
class TestGetProgramsSubscriptionData(TestCase):
|
||||
"""
|
||||
Tests for the get_programs_subscription_data utility function.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.mock_program_subscription_data = [
|
||||
{'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
|
||||
'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T00:02:03Z',
|
||||
'price': '100.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_1',
|
||||
'current_period_end': '1970-01-01T00:02:03Z', 'status': 'active',
|
||||
'customer': 1, 'subscription_state': 'active'},
|
||||
{'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
|
||||
'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T03:25:12Z',
|
||||
'price': '1000.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_2',
|
||||
'current_period_end': '1970-05-23T12:05:21Z', 'status': 'subscription_initiated',
|
||||
'customer': 1, 'subscription_state': 'notStarted'}
|
||||
]
|
||||
|
||||
@mock.patch(UTILS_MODULE + ".get_subscription_api_client")
|
||||
@mock.patch(UTILS_MODULE + ".log.info")
|
||||
def test_get_programs_subscription_data(self, mock_log, mock_get_subscription_api_client):
|
||||
# mock return values
|
||||
mock_client = mock.Mock()
|
||||
mock_get_subscription_api_client.return_value = mock_client
|
||||
mock_response = {"results": self.mock_program_subscription_data, "next": None}
|
||||
mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
|
||||
|
||||
# call the function
|
||||
user = mock.Mock()
|
||||
result = get_programs_subscription_data(user)
|
||||
|
||||
# assert expected behavior
|
||||
mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}")
|
||||
mock_get_subscription_api_client.assert_called_once_with(user)
|
||||
mock_client.get.assert_called_once_with(settings.SUBSCRIPTIONS_API_PATH, params={"page": 1})
|
||||
assert result == self.mock_program_subscription_data
|
||||
|
||||
@mock.patch(UTILS_MODULE + ".get_subscription_api_client")
|
||||
@mock.patch(UTILS_MODULE + ".log.info")
|
||||
def test_get_programs_subscription_data_with_uuid(self, mock_log, mock_get_subscription_api_client):
|
||||
mock_client = mock.Mock()
|
||||
mock_get_subscription_api_client.return_value = mock_client
|
||||
subscription_data = self.mock_program_subscription_data[0]
|
||||
program_uuid = subscription_data['resource_id']
|
||||
|
||||
mock_response = {"results": subscription_data, "next": None}
|
||||
mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
|
||||
|
||||
user = mock.Mock()
|
||||
result = get_programs_subscription_data(user, program_uuid=program_uuid)
|
||||
|
||||
mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}"
|
||||
f" for program_uuid: {str(program_uuid)}")
|
||||
mock_get_subscription_api_client.assert_called_once_with(user)
|
||||
mock_client.get.assert_called_once_with(
|
||||
settings.SUBSCRIPTIONS_API_PATH,
|
||||
params={
|
||||
"most_active_and_recent": 'true',
|
||||
"resource_id": program_uuid,
|
||||
}
|
||||
)
|
||||
assert result == subscription_data
|
||||
|
||||
|
||||
@override_settings(SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL='http://subscription_buy_url/')
|
||||
@ddt.ddt
|
||||
class TestBuySubscriptionUrl(TestCase):
|
||||
"""
|
||||
Tests for the BuySubscriptionUrl utility function.
|
||||
"""
|
||||
@ddt.data(
|
||||
{
|
||||
'skus': ['TESTSKU'],
|
||||
'program_uuid': '12345678-9012-3456-7890-123456789012'
|
||||
},
|
||||
{
|
||||
'skus': ['TESTSKU1', 'TESTSKU2', 'TESTSKU3'],
|
||||
'program_uuid': '12345678-9012-3456-7890-123456789012'
|
||||
},
|
||||
{
|
||||
'skus': [],
|
||||
'program_uuid': '12345678-9012-3456-7890-123456789012'
|
||||
}
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_buy_subscription_url(self, skus, program_uuid):
|
||||
""" Verify the subscription purchase page URL is properly constructed and returned. """
|
||||
url = get_buy_subscription_url(program_uuid, skus)
|
||||
formatted_skus = urlencode({'sku': skus}, doseq=True)
|
||||
expected_url = f'{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}'
|
||||
assert url == expected_url
|
||||
|
||||
@@ -5,9 +5,8 @@ import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
from urllib.parse import urlencode, urljoin, urlparse, urlunparse
|
||||
from urllib.parse import urljoin, urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -15,7 +14,6 @@ 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
|
||||
from edx_rest_api_client.auth import SuppliedJwtAuth
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import utc
|
||||
from requests.exceptions import RequestException
|
||||
@@ -42,7 +40,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_records_url
|
||||
from openedx.core.djangoapps.enrollments.api import get_enrollments
|
||||
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -64,15 +61,6 @@ def get_program_and_course_data(site, user, program_uuid, mobile_only=False):
|
||||
return program_data, course_data
|
||||
|
||||
|
||||
def get_buy_subscription_url(program_uuid, skus):
|
||||
"""
|
||||
Returns the URL to the Subscription Purchase page for the given program UUID and course Skus.
|
||||
"""
|
||||
formatted_skus = urlencode({"sku": skus}, doseq=True)
|
||||
url = f"{settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL}{program_uuid}/?{formatted_skus}"
|
||||
return url
|
||||
|
||||
|
||||
def get_program_urls(program_data):
|
||||
"""Returns important urls of program."""
|
||||
from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id
|
||||
@@ -92,10 +80,6 @@ def get_program_urls(program_data):
|
||||
"commerce_api_url": reverse("commerce_api:v0:baskets:create"),
|
||||
"buy_button_url": ecommerce_service.get_checkout_page_url(*skus),
|
||||
"program_record_url": program_record_url,
|
||||
"buy_subscription_url": get_buy_subscription_url(program_uuid, skus),
|
||||
"manage_subscription_url": settings.SUBSCRIPTIONS_MANAGE_SUBSCRIPTION_URL,
|
||||
"orders_and_subscriptions_url": settings.ORDER_HISTORY_MICROFRONTEND_URL,
|
||||
"subscriptions_learner_help_center_url": settings.SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL,
|
||||
}
|
||||
return urls
|
||||
|
||||
@@ -129,15 +113,6 @@ def get_program_marketing_url(programs_config, mobile_only=False):
|
||||
return marketing_url
|
||||
|
||||
|
||||
def get_program_subscriptions_marketing_url():
|
||||
"""Build a URL used to link to subscription eligible programs on the marketing site."""
|
||||
marketing_urls = settings.MKTG_URLS
|
||||
return urljoin(
|
||||
marketing_urls.get("ROOT"),
|
||||
marketing_urls.get("PROGRAM_SUBSCRIPTIONS"),
|
||||
)
|
||||
|
||||
|
||||
def attach_program_detail_url(programs, mobile_only=False):
|
||||
"""Extend program representations by attaching a URL to be used when linking to program details.
|
||||
|
||||
@@ -1042,51 +1017,3 @@ def is_user_enrolled_in_program_type(
|
||||
elif course_run_id in course_runs:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_subscription_api_client(user):
|
||||
"""
|
||||
Returns an API client which can be used to make Subscriptions API requests.
|
||||
"""
|
||||
scopes = ["user_id", "email", "profile"]
|
||||
jwt = create_jwt_for_user(user, scopes=scopes)
|
||||
client = requests.Session()
|
||||
client.auth = SuppliedJwtAuth(jwt)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def get_programs_subscription_data(user, program_uuid=None):
|
||||
"""
|
||||
Returns the subscription data for a user's program if uuid is specified
|
||||
else return data for user's all subscriptions.
|
||||
"""
|
||||
client = get_subscription_api_client(user)
|
||||
api_path = f"{settings.SUBSCRIPTIONS_API_PATH}"
|
||||
subscription_data = []
|
||||
|
||||
log.info(
|
||||
f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}"
|
||||
+ (f" for program_uuid: {program_uuid}" if program_uuid is not None else "")
|
||||
)
|
||||
|
||||
try:
|
||||
if program_uuid:
|
||||
response = client.get(api_path, params={"resource_id": program_uuid, "most_active_and_recent": "true"})
|
||||
response.raise_for_status()
|
||||
subscription_data = response.json().get("results", [])
|
||||
else:
|
||||
next_page = 1
|
||||
while next_page:
|
||||
response = client.get(api_path, params=dict(page=next_page))
|
||||
response.raise_for_status()
|
||||
subscription_data.extend(response.json().get("results", []))
|
||||
next_page = response.json().get("next")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
log.exception(
|
||||
f"B2C_SUBSCRIPTIONS: Failed to retrieve Program Subscription Data for user: {user} with error: {exc}"
|
||||
+ f" for program_uuid: {str(program_uuid)}"
|
||||
if program_uuid is not None
|
||||
else ""
|
||||
)
|
||||
return subscription_data
|
||||
|
||||
Reference in New Issue
Block a user