Commit a154e7f1 by Awais

Adding the ecom api functionality for the credentials.

ECOM-2931
parent f3d000ec
...@@ -4,6 +4,7 @@ import datetime ...@@ -4,6 +4,7 @@ import datetime
import json import json
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from freezegun import freeze_time from freezegun import freeze_time
...@@ -99,3 +100,13 @@ class EdxRestApiClientTest(TestCase): ...@@ -99,3 +100,13 @@ class EdxRestApiClientTest(TestCase):
) )
actual_object = ecommerce_api_client(self.user).baskets(1).order.get() actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
self.assertEqual(actual_object, {u"result": u"Préparatoire"}) self.assertEqual(actual_object, {u"result": u"Préparatoire"})
def test_client_with_user_without_profile(self):
"""
Verify client initialize successfully for users having no profile.
"""
worker = User.objects.create_user(username='test_worker', email='test@example.com')
api_client = ecommerce_api_client(worker)
self.assertEqual(api_client._store['session'].auth.__dict__['username'], worker.username) # pylint: disable=protected-access
self.assertIsNone(api_client._store['session'].auth.__dict__['full_name']) # pylint: disable=protected-access
...@@ -34,13 +34,25 @@ ...@@ -34,13 +34,25 @@
</p> </p>
<p> <p>
${_(u"Congratulations! You are eligible to receive course credit for successfully completing your {platform_name} course! {link_start}Get your credit now.{link_end}").format(
link_start=u'<a href="{dashboard_url}">'.format( % if providers:
dashboard_url=dashboard_link ${_(u"Congratulations! You are eligible to receive course credit from {providers} for successfully completing your {platform_name} course! {link_start}Get your credit now.{link_end}").format(
), link_start=u'<a href="{dashboard_url}">'.format(
link_end=u'</a>', dashboard_url=dashboard_link
platform_name=settings.PLATFORM_NAME ),
)} link_end=u'</a>',
platform_name=settings.PLATFORM_NAME,
providers=providers
)}
% else:
${_(u"Congratulations! You are eligible to receive course credit for successfully completing your {platform_name} course! {link_start}Get your credit now.{link_end}").format(
link_start=u'<a href="{dashboard_url}">'.format(
dashboard_url=dashboard_link
),
link_end=u'</a>',
platform_name=settings.PLATFORM_NAME
)}
% endif
</p> </p>
<p> <p>
......
...@@ -5,7 +5,11 @@ ${_(u"Hi {name},").format(name=full_name)} ...@@ -5,7 +5,11 @@ ${_(u"Hi {name},").format(name=full_name)}
${_(u"Hi,")} ${_(u"Hi,")}
% endif % endif
${_(u"Congratulations! You are eligible to receive course credit for successfully completing your edX course!")} % if providers:
${_(u"Congratulations! You are eligible to receive course credit from {providers} for successfully completing your edX course!").format(providers=providers)}
% else:
${_(u"Congratulations! You are eligible to receive course credit for successfully completing your edX course!")}
% endif
${_(u"Click on the link below to get your credit now:")} ${_(u"Click on the link below to get your credit now:")}
......
...@@ -28,11 +28,13 @@ def is_commerce_service_configured(): ...@@ -28,11 +28,13 @@ def is_commerce_service_configured():
def ecommerce_api_client(user): def ecommerce_api_client(user):
""" Returns an E-Commerce API client setup with authentication for the specified user. """ """ Returns an E-Commerce API client setup with authentication for the specified user. """
return EdxRestApiClient(settings.ECOMMERCE_API_URL, return EdxRestApiClient(
settings.ECOMMERCE_API_SIGNING_KEY, settings.ECOMMERCE_API_URL,
user.username, settings.ECOMMERCE_API_SIGNING_KEY,
user.profile.name, user.username,
user.email, user.profile.name if hasattr(user, 'profile') else None,
tracking_context=create_tracking_context(user), user.email,
issuer=settings.JWT_ISSUER, tracking_context=create_tracking_context(user),
expires_in=settings.JWT_EXPIRATION) issuer=settings.JWT_ISSUER,
expires_in=settings.JWT_EXPIRATION
)
...@@ -3,7 +3,7 @@ Django admin page for credit eligibility ...@@ -3,7 +3,7 @@ Django admin page for credit eligibility
""" """
from ratelimitbackend import admin from ratelimitbackend import admin
from openedx.core.djangoapps.credit.models import ( from openedx.core.djangoapps.credit.models import (
CreditCourse, CreditProvider, CreditEligibility, CreditRequest CreditConfig, CreditCourse, CreditProvider, CreditEligibility, CreditRequest
) )
...@@ -51,3 +51,4 @@ admin.site.register(CreditCourse, CreditCourseAdmin) ...@@ -51,3 +51,4 @@ admin.site.register(CreditCourse, CreditCourseAdmin)
admin.site.register(CreditProvider, CreditProviderAdmin) admin.site.register(CreditProvider, CreditProviderAdmin)
admin.site.register(CreditEligibility, CreditEligibilityAdmin) admin.site.register(CreditEligibility, CreditEligibilityAdmin)
admin.site.register(CreditRequest, CreditRequestAdmin) admin.site.register(CreditRequest, CreditRequestAdmin)
admin.site.register(CreditConfig)
...@@ -21,10 +21,11 @@ from django.utils.translation import ugettext as _ ...@@ -21,10 +21,11 @@ from django.utils.translation import ugettext as _
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from eventtracking import tracker from eventtracking import tracker
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from edxmako.template import Template from edxmako.template import Template
from microsite_configuration import microsite from microsite_configuration import microsite
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
from openedx.core.djangoapps.credit.models import CreditConfig, CreditProvider
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -67,6 +68,26 @@ def send_credit_notifications(username, course_key): ...@@ -67,6 +68,26 @@ def send_credit_notifications(username, course_key):
# strip enclosing angle brackets from 'logo_image' cache 'Content-ID' # strip enclosing angle brackets from 'logo_image' cache 'Content-ID'
logo_image_id = logo_image.get('Content-ID', '')[1:-1] logo_image_id = logo_image.get('Content-ID', '')[1:-1]
providers = get_credit_provider_display_names(course_key)
providers_string = None
if providers:
if len(providers) > 1:
if len(providers) > 2:
# Translators: The join of three or more university names. The first of these formatting strings
# represents a comma-separated list of names (e.g., MIT, Harvard, Dartmouth).
providers_string = _("{first_providers}, and {last_provider}").format(
first_providers=u", ".join(providers[:-1]),
last_provider=providers[-1]
)
else:
# Translators: The join of two university names (e.g., Harvard and MIT).
providers_string = _("{first_provider} and {second_provider}").format(
first_provider=providers[0],
second_provider=providers[1]
)
else:
providers_string = providers[0]
context = { context = {
'full_name': user.get_full_name(), 'full_name': user.get_full_name(),
'platform_name': settings.PLATFORM_NAME, 'platform_name': settings.PLATFORM_NAME,
...@@ -75,6 +96,7 @@ def send_credit_notifications(username, course_key): ...@@ -75,6 +96,7 @@ def send_credit_notifications(username, course_key):
'dashboard_link': dashboard_link, 'dashboard_link': dashboard_link,
'credit_course_link': credit_course_link, 'credit_course_link': credit_course_link,
'tracking_pixel': tracking_pixel, 'tracking_pixel': tracking_pixel,
'providers': providers_string,
} }
# create the root email message # create the root email message
...@@ -85,6 +107,10 @@ def send_credit_notifications(username, course_key): ...@@ -85,6 +107,10 @@ def send_credit_notifications(username, course_key):
notification_msg.attach(msg_alternative) notification_msg.attach(msg_alternative)
# render the credit notification templates # render the credit notification templates
subject = _(u'Course Credit Eligibility') subject = _(u'Course Credit Eligibility')
if providers:
subject = _(u'You are eligible for credit from {providers_string}').format(
providers_string=providers_string
)
# add alternative plain text message # add alternative plain text message
email_body_plain = render_to_string('credit_notifications/credit_eligibility_email.txt', context) email_body_plain = render_to_string('credit_notifications/credit_eligibility_email.txt', context)
...@@ -180,3 +206,56 @@ def _email_url_parser(url_name, extra_param=None): ...@@ -180,3 +206,56 @@ def _email_url_parser(url_name, extra_param=None):
dashboard_url_path = reverse(url_name) + extra_param if extra_param else reverse(url_name) dashboard_url_path = reverse(url_name) + extra_param if extra_param else reverse(url_name)
dashboard_link_parts = ("https", site_name, dashboard_url_path, '', '', '') dashboard_link_parts = ("https", site_name, dashboard_url_path, '', '', '')
return urlparse.urlunparse(dashboard_link_parts) return urlparse.urlunparse(dashboard_link_parts)
def get_credit_provider_display_names(course_key):
"""Get the course information from ecommerce and parse the data to get providers.
Arguments:
course_key (CourseKey): The identifier for the course.
Returns:
List of credit provider display names.
"""
course_id = unicode(course_key)
credit_config = CreditConfig.current()
cache_key = None
provider_names = None
if credit_config.is_cache_enabled:
cache_key = '{key_prefix}.{course_key}'.format(
key_prefix=credit_config.CACHE_KEY, course_key=course_id
)
provider_names = cache.get(cache_key)
if provider_names is not None:
return provider_names
try:
user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
response = ecommerce_api_client(user).courses(course_id).get(include_products=1)
except Exception: # pylint: disable=broad-except
log.exception("Failed to receive data from the ecommerce course API for Course ID '%s'.", course_id)
return provider_names
if not response:
log.info("No Course information found from ecommerce API for Course ID '%s'.", course_id)
return provider_names
provider_ids = []
for product in response.get('products'):
provider_ids += [
attr.get('value') for attr in product.get('attribute_values') if attr.get('name') == 'credit_provider'
]
provider_names = []
credit_providers = CreditProvider.get_credit_providers()
for provider in credit_providers:
if provider['id'] in provider_ids:
provider_names.append(provider['display_name'])
if credit_config.is_cache_enabled:
cache.set(cache_key, provider_names, credit_config.cache_ttl)
return provider_names
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('credit', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CreditConfig',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('cache_ttl', models.PositiveIntegerField(default=0, help_text='Specified in seconds. Enable caching by setting this to a value greater than 0.', verbose_name='Cache Time To Live')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]
...@@ -6,23 +6,22 @@ Credit courses allow students to receive university credit for ...@@ -6,23 +6,22 @@ Credit courses allow students to receive university credit for
successful completion of a course on EdX successful completion of a course on EdX
""" """
import datetime
from collections import defaultdict from collections import defaultdict
import datetime
import logging import logging
import pytz from config_models.models import ConfigurationModel
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.dispatch import receiver
from django.db import models, transaction, IntegrityError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from simple_history.models import HistoricalRecords from django.db import models, transaction, IntegrityError
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy, ugettext as _
from jsonfield.fields import JSONField from jsonfield.fields import JSONField
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
import pytz
from simple_history.models import HistoricalRecords
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
from django.utils.translation import ugettext_lazy
CREDIT_PROVIDER_ID_REGEX = r"[a-z,A-Z,0-9,\-]+" CREDIT_PROVIDER_ID_REGEX = r"[a-z,A-Z,0-9,\-]+"
...@@ -709,3 +708,25 @@ class CreditRequest(TimeStampedModel): ...@@ -709,3 +708,25 @@ class CreditRequest(TimeStampedModel):
provider=self.provider.provider_id, provider=self.provider.provider_id,
status=self.status, status=self.status,
) )
class CreditConfig(ConfigurationModel):
""" Manage credit configuration """
CACHE_KEY = 'credit.providers.api.data'
cache_ttl = models.PositiveIntegerField(
verbose_name=_("Cache Time To Live"),
default=0,
help_text=_(
"Specified in seconds. Enable caching by setting this to a value greater than 0."
)
)
@property
def is_cache_enabled(self):
"""Whether responses from the commerce API will be cached."""
return self.enabled and self.cache_ttl > 0
def __unicode__(self):
"""Unicode representation of the config. """
return 'Credit Configuration'
...@@ -2,21 +2,23 @@ ...@@ -2,21 +2,23 @@
Tests for the API functions in the credit app. Tests for the API functions in the credit app.
""" """
import datetime import datetime
import json
import unittest import unittest
import ddt import ddt
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail from django.core import mail
from django.test.utils import override_settings from django.test.utils import override_settings
from django.db import connection, transaction from django.db import connection
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey import httpretty
from lms.djangoapps.commerce.tests import TEST_API_SIGNING_KEY, TEST_API_URL
import mock
import pytz import pytz
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.tests.factories import CourseFactory
from util.date_utils import from_timestamp
from openedx.core.djangoapps.credit import api from openedx.core.djangoapps.credit import api
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names
from openedx.core.djangoapps.credit.exceptions import ( from openedx.core.djangoapps.credit.exceptions import (
InvalidCreditRequirements, InvalidCreditRequirements,
InvalidCreditCourse, InvalidCreditCourse,
...@@ -26,6 +28,7 @@ from openedx.core.djangoapps.credit.exceptions import ( ...@@ -26,6 +28,7 @@ from openedx.core.djangoapps.credit.exceptions import (
CreditRequestNotFound, CreditRequestNotFound,
) )
from openedx.core.djangoapps.credit.models import ( from openedx.core.djangoapps.credit.models import (
CreditConfig,
CreditCourse, CreditCourse,
CreditProvider, CreditProvider,
CreditRequirement, CreditRequirement,
...@@ -33,8 +36,13 @@ from openedx.core.djangoapps.credit.models import ( ...@@ -33,8 +36,13 @@ from openedx.core.djangoapps.credit.models import (
CreditEligibility CreditEligibility
) )
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util.date_utils import from_timestamp
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6" TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6"
TEST_ECOMMERCE_WORKER = 'test_worker'
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={ @override_settings(CREDIT_PROVIDER_SECRET_KEYS={
...@@ -64,6 +72,86 @@ class CreditApiTestBase(ModuleStoreTestCase): ...@@ -64,6 +72,86 @@ class CreditApiTestBase(ModuleStoreTestCase):
} }
THUMBNAIL_URL = "https://credit.example.com/logo.png" THUMBNAIL_URL = "https://credit.example.com/logo.png"
PROVIDERS_LIST = [u'Hogwarts School of Witchcraft and Wizardry', u'Arizona State University']
COURSE_API_RESPONSE = {
"id": "course-v1:Demo+Demox+Course",
"url": "http://localhost/api/v2/courses/course-v1:Demo+Demox+Course/",
"name": "dummy edX Demonstration Course",
"verification_deadline": "2023-09-12T23:59:00Z",
"type": "credit",
"products_url": "http://localhost/api/v2/courses/course:Demo+Demox+Course/products/",
"last_edited": "2016-03-06T09:51:10Z",
"products": [
{
"id": 1,
"url": "http://localhost/api/v2/products/11/",
"structure": "child",
"product_class": "Seat",
"title": "",
"price": 1,
"expires": '2016-03-06T09:51:10Z',
"attribute_values": [
{
"name": "certificate_type",
"value": "credit"
},
{
"name": "course_key",
"value": "edX/DemoX/Demo_Course",
},
{
"name": "credit_hours",
"value": 1
},
{
"name": "credit_provider",
"value": "ASU"
},
{
"name": "id_verification_required",
"value": False
}
],
"is_available_to_buy": False,
"stockrecords": []
},
{
"id": 2,
"url": "http://localhost/api/v2/products/10/",
"structure": "child",
"product_class": "Seat",
"title": "",
"price": 1,
"expires": '2016-03-06T09:51:10Z',
"attribute_values": [
{
"name": "certificate_type",
"value": "credit"
},
{
"name": "course_key",
"value": "edX/DemoX/Demo_Course",
},
{
"name": "credit_hours",
"value": 1
},
{
"name": "credit_provider",
"value": PROVIDER_ID
},
{
"name": "id_verification_required",
"value": False
}
],
"is_available_to_buy": False,
"stockrecords": []
}
]
}
def setUp(self, **kwargs): def setUp(self, **kwargs):
super(CreditApiTestBase, self).setUp() super(CreditApiTestBase, self).setUp()
self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course") self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course")
...@@ -86,6 +174,15 @@ class CreditApiTestBase(ModuleStoreTestCase): ...@@ -86,6 +174,15 @@ class CreditApiTestBase(ModuleStoreTestCase):
return credit_course return credit_course
def _mock_ecommerce_courses_api(self, course_key, body, status=200):
""" Mock GET requests to the ecommerce course API endpoint. """
httpretty.reset()
httpretty.register_uri(
httpretty.GET, '{}/courses/{}/?include_products=1'.format(TEST_API_URL, unicode(course_key)),
status=status,
body=json.dumps(body), content_type='application/json',
)
@attr('shard_2') @attr('shard_2')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
...@@ -394,10 +491,20 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -394,10 +491,20 @@ class CreditRequirementApiTests(CreditApiTestBase):
req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade")
self.assertEqual(len(req_status), 0) self.assertEqual(len(req_status), 0)
@httpretty.activate
@override_settings(
ECOMMERCE_API_URL=TEST_API_URL,
ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY,
ECOMMERCE_SERVICE_WORKER_USERNAME=TEST_ECOMMERCE_WORKER
)
def test_satisfy_all_requirements(self): def test_satisfy_all_requirements(self):
""" Test the credit requirements, eligibility notification, email """ Test the credit requirements, eligibility notification, email
content caching for a credit course. content caching for a credit course.
""" """
self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE)
worker_user = User.objects.create_user(username=TEST_ECOMMERCE_WORKER)
self.assertFalse(hasattr(worker_user, 'profile'))
# Configure a course with two credit requirements # Configure a course with two credit requirements
self.add_credit_course() self.add_credit_course()
CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
...@@ -435,7 +542,7 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -435,7 +542,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key))
# Satisfy the other requirement # Satisfy the other requirement
with self.assertNumQueries(15): with self.assertNumQueries(19):
api.set_credit_requirement_status( api.set_credit_requirement_status(
"bob", "bob",
self.course_key, self.course_key,
...@@ -448,7 +555,10 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -448,7 +555,10 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Credit eligibility email should be sent # Credit eligibility email should be sent
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility') self.assertEqual(
mail.outbox[0].subject,
'You are eligible for credit from Hogwarts School of Witchcraft and Wizardry'
)
# Now verify them email content # Now verify them email content
email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access
...@@ -471,11 +581,22 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -471,11 +581,22 @@ class CreditRequirementApiTests(CreditApiTestBase):
image_id = email_image.get('Content-ID', '')[1:-1] image_id = email_image.get('Content-ID', '')[1:-1]
self.assertIsNotNone(image_id) self.assertIsNotNone(image_id)
self.assertIn(image_id, html_content_first) self.assertIn(image_id, html_content_first)
self.assertIn(
'credit from Hogwarts School of Witchcraft and Wizardry for',
html_content_first
)
# test text email contents
text_content_first = email_payload_first[0]._payload[0]._payload
self.assertIn(
'credit from Hogwarts School of Witchcraft and Wizardry for',
text_content_first
)
# Delete the eligibility entries and satisfy the user's eligibility # Delete the eligibility entries and satisfy the user's eligibility
# requirement again to trigger eligibility notification # requirement again to trigger eligibility notification
CreditEligibility.objects.all().delete() CreditEligibility.objects.all().delete()
with self.assertNumQueries(13): with self.assertNumQueries(15):
api.set_credit_requirement_status( api.set_credit_requirement_status(
"bob", "bob",
self.course_key, self.course_key,
...@@ -551,6 +672,87 @@ class CreditRequirementApiTests(CreditApiTestBase): ...@@ -551,6 +672,87 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertEqual(len(req_status), 1) self.assertEqual(len(req_status), 1)
self.assertEqual(req_status[0]["status"], None) self.assertEqual(req_status[0]["status"], None)
@ddt.data(
(
[u'Arizona State University'],
'credit from Arizona State University for',
'You are eligible for credit from Arizona State University'),
(
[u'Arizona State University', u'Hogwarts School of Witchcraft and Wizardry'],
'credit from Arizona State University and Hogwarts School of Witchcraft and Wizardry for',
'You are eligible for credit from Arizona State University and Hogwarts School of Witchcraft and Wizardry'
),
(
[u'Arizona State University', u'Hogwarts School of Witchcraft and Wizardry', u'Charter Oak'],
'credit from Arizona State University, Hogwarts School of Witchcraft and Wizardry, and Charter Oak for',
'You are eligible for credit from Arizona State University, Hogwarts School'
' of Witchcraft and Wizardry, and Charter Oak'
),
([], 'credit for', 'Course Credit Eligibility'),
(None, 'credit for', 'Course Credit Eligibility')
)
@ddt.unpack
def test_eligibility_email_with_providers(self, providers_list, providers_email_message, expected_subject):
""" Test the credit requirements, eligibility notification, email
for different providers combinations.
"""
# Configure a course with two credit requirements
self.add_credit_course()
CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
requirements = [
{
"namespace": "grade",
"name": "grade",
"display_name": "Grade",
"criteria": {
"min_grade": 0.8
},
},
{
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "Assessment 1",
"criteria": {},
}
]
api.set_credit_requirements(self.course_key, requirements)
user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password'])
# Satisfy one of the requirements, but not the other
api.set_credit_requirement_status(
user.username,
self.course_key,
requirements[0]["namespace"],
requirements[0]["name"]
)
# Satisfy the other requirement. And mocked the api to return different kind of data.
with mock.patch('openedx.core.djangoapps.credit.email_utils.get_credit_provider_display_names') as mock_method:
mock_method.return_value = providers_list
api.set_credit_requirement_status(
"bob",
self.course_key,
requirements[1]["namespace"],
requirements[1]["name"]
)
# Now the user should be eligible
self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key))
# Credit eligibility email should be sent
self.assertEqual(len(mail.outbox), 1)
# Verify the email subject
self.assertEqual(mail.outbox[0].subject, expected_subject)
# Now verify them email content
email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access
html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access
self.assertIn(providers_email_message, html_content_first)
# test text email
text_content_first = email_payload_first[0]._payload[0]._payload # pylint: disable=protected-access
self.assertIn(providers_email_message, text_content_first)
@attr('shard_2') @attr('shard_2')
@ddt.ddt @ddt.ddt
...@@ -879,3 +1081,126 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): ...@@ -879,3 +1081,126 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
"""Check the user's credit status. """ """Check the user's credit status. """
statuses = api.get_credit_requests_for_user(self.USER_INFO["username"]) statuses = api.get_credit_requests_for_user(self.USER_INFO["username"])
self.assertEqual(statuses[0]["status"], expected_status) self.assertEqual(statuses[0]["status"], expected_status)
@attr('shard_2')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
@override_settings(
ECOMMERCE_API_URL=TEST_API_URL,
ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY,
ECOMMERCE_SERVICE_WORKER_USERNAME=TEST_ECOMMERCE_WORKER
)
@ddt.ddt
class CourseApiTests(CreditApiTestBase):
"""Test Python API for course product information."""
def setUp(self):
super(CourseApiTests, self).setUp()
self.worker_user = User.objects.create_user(username=TEST_ECOMMERCE_WORKER)
self.add_credit_course(self.course_key)
self.credit_config = CreditConfig(cache_ttl=100, enabled=True)
self.credit_config.save()
# Add another provider.
CreditProvider.objects.get_or_create(
provider_id='ASU',
display_name='Arizona State University',
provider_url=self.PROVIDER_URL,
provider_status_url=self.PROVIDER_STATUS_URL,
provider_description=self.PROVIDER_DESCRIPTION,
enable_integration=self.ENABLE_INTEGRATION,
fulfillment_instructions=self.FULFILLMENT_INSTRUCTIONS,
thumbnail_url=self.THUMBNAIL_URL
)
self.assertFalse(hasattr(self.worker_user, 'profile'))
@httpretty.activate
def test_get_credit_provider_display_names_method(self):
"""Verify that parsed providers list is returns after getting course production information."""
self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE)
response_providers = get_credit_provider_display_names(self.course_key)
self.assertListEqual(self.PROVIDERS_LIST, response_providers)
@httpretty.activate
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_credit_provider_display_names_method_with_exception(self, mock_init):
"""Verify that in case of any exception it logs the error and return."""
mock_init.side_effect = Exception
response = get_credit_provider_display_names(self.course_key)
self.assertTrue(mock_init.called)
self.assertEqual(response, None)
@httpretty.activate
def test_get_credit_provider_display_names_caching(self):
"""Verify that providers list is cached."""
self.assertTrue(self.credit_config.is_cache_enabled)
self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE)
# Warm up the cache.
response_providers = get_credit_provider_display_names(self.course_key)
self.assertListEqual(self.PROVIDERS_LIST, response_providers)
# Hit the cache.
response_providers = get_credit_provider_display_names(self.course_key)
self.assertListEqual(self.PROVIDERS_LIST, response_providers)
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate
def test_get_credit_provider_display_names_without_caching(self):
"""Verify that providers list is not cached."""
self.credit_config.cache_ttl = 0
self.credit_config.save()
self.assertFalse(self.credit_config.is_cache_enabled)
self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE)
response_providers = get_credit_provider_display_names(self.course_key)
self.assertListEqual(self.PROVIDERS_LIST, response_providers)
response_providers = get_credit_provider_display_names(self.course_key)
self.assertListEqual(self.PROVIDERS_LIST, response_providers)
self.assertEqual(len(httpretty.httpretty.latest_requests), 2)
@httpretty.activate
@ddt.data(
(None, None),
({'products': []}, []),
(
{
'products': [{'expires': '', 'attribute_values': [{'name': 'credit_provider', 'value': 'ASU'}]}]
}, ['Arizona State University']
),
(
{
'products': [{'expires': '', 'attribute_values': [{'name': 'namespace', 'value': 'grade'}]}]
}, []
),
(
{
'products': [
{
'expires': '', 'attribute_values':
[
{'name': 'credit_provider', 'value': 'ASU'},
{'name': 'credit_provider', 'value': 'hogwarts'},
{'name': 'course_type', 'value': 'credit'}
]
}
]
}, ['Hogwarts School of Witchcraft and Wizardry', 'Arizona State University']
)
)
@ddt.unpack
def test_get_provider_api_with_multiple_data(self, data, expected_data):
self._mock_ecommerce_courses_api(self.course_key, data)
response_providers = get_credit_provider_display_names(self.course_key)
self.assertEqual(expected_data, response_providers)
@httpretty.activate
def test_get_credit_provider_display_names_without_providers(self):
"""Verify that if all providers are in-active than method return empty list."""
self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE)
CreditProvider.objects.all().update(active=False)
self.assertEqual(get_credit_provider_display_names(self.course_key), [])
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