Merge pull request #11759 from edx/awais786/ECOM-2931-update-credit-eligible-email
Add the providers information in the email.
This commit is contained in:
@@ -4,6 +4,7 @@ import datetime
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from freezegun import freeze_time
|
||||
@@ -99,3 +100,13 @@ class EdxRestApiClientTest(TestCase):
|
||||
)
|
||||
actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
|
||||
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 @@
|
||||
</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(
|
||||
dashboard_url=dashboard_link
|
||||
),
|
||||
link_end=u'</a>',
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
)}
|
||||
|
||||
% if providers:
|
||||
${_(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(
|
||||
dashboard_url=dashboard_link
|
||||
),
|
||||
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>
|
||||
|
||||
@@ -5,7 +5,11 @@ ${_(u"Hi {name},").format(name=full_name)}
|
||||
${_(u"Hi,")}
|
||||
% 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:")}
|
||||
|
||||
|
||||
@@ -28,11 +28,13 @@ def is_commerce_service_configured():
|
||||
|
||||
def ecommerce_api_client(user):
|
||||
""" Returns an E-Commerce API client setup with authentication for the specified user. """
|
||||
return EdxRestApiClient(settings.ECOMMERCE_API_URL,
|
||||
settings.ECOMMERCE_API_SIGNING_KEY,
|
||||
user.username,
|
||||
user.profile.name,
|
||||
user.email,
|
||||
tracking_context=create_tracking_context(user),
|
||||
issuer=settings.JWT_ISSUER,
|
||||
expires_in=settings.JWT_EXPIRATION)
|
||||
return EdxRestApiClient(
|
||||
settings.ECOMMERCE_API_URL,
|
||||
settings.ECOMMERCE_API_SIGNING_KEY,
|
||||
user.username,
|
||||
user.profile.name if hasattr(user, 'profile') else None,
|
||||
user.email,
|
||||
tracking_context=create_tracking_context(user),
|
||||
issuer=settings.JWT_ISSUER,
|
||||
expires_in=settings.JWT_EXPIRATION
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ Django admin page for credit eligibility
|
||||
"""
|
||||
from ratelimitbackend import admin
|
||||
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)
|
||||
admin.site.register(CreditProvider, CreditProviderAdmin)
|
||||
admin.site.register(CreditEligibility, CreditEligibilityAdmin)
|
||||
admin.site.register(CreditRequest, CreditRequestAdmin)
|
||||
admin.site.register(CreditConfig)
|
||||
|
||||
@@ -21,10 +21,11 @@ from django.utils.translation import ugettext as _
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from eventtracking import tracker
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from edxmako.template import Template
|
||||
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
|
||||
|
||||
|
||||
@@ -67,6 +68,26 @@ def send_credit_notifications(username, course_key):
|
||||
# strip enclosing angle brackets from 'logo_image' cache 'Content-ID'
|
||||
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 = {
|
||||
'full_name': user.get_full_name(),
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
@@ -75,6 +96,7 @@ def send_credit_notifications(username, course_key):
|
||||
'dashboard_link': dashboard_link,
|
||||
'credit_course_link': credit_course_link,
|
||||
'tracking_pixel': tracking_pixel,
|
||||
'providers': providers_string,
|
||||
}
|
||||
|
||||
# create the root email message
|
||||
@@ -85,6 +107,10 @@ def send_credit_notifications(username, course_key):
|
||||
notification_msg.attach(msg_alternative)
|
||||
# render the credit notification templates
|
||||
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
|
||||
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):
|
||||
dashboard_url_path = reverse(url_name) + extra_param if extra_param else reverse(url_name)
|
||||
dashboard_link_parts = ("https", site_name, dashboard_url_path, '', '', '')
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- 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
|
||||
successful completion of a course on EdX
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import pytz
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.conf import settings
|
||||
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 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 model_utils.models import TimeStampedModel
|
||||
import pytz
|
||||
from simple_history.models import HistoricalRecords
|
||||
from xmodule_django.models import CourseKeyField
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
CREDIT_PROVIDER_ID_REGEX = r"[a-z,A-Z,0-9,\-]+"
|
||||
@@ -709,3 +708,25 @@ class CreditRequest(TimeStampedModel):
|
||||
provider=self.provider.provider_id,
|
||||
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 @@
|
||||
Tests for the API functions in the credit app.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
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 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
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from util.date_utils import from_timestamp
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
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 (
|
||||
InvalidCreditRequirements,
|
||||
InvalidCreditCourse,
|
||||
@@ -26,6 +28,7 @@ from openedx.core.djangoapps.credit.exceptions import (
|
||||
CreditRequestNotFound,
|
||||
)
|
||||
from openedx.core.djangoapps.credit.models import (
|
||||
CreditConfig,
|
||||
CreditCourse,
|
||||
CreditProvider,
|
||||
CreditRequirement,
|
||||
@@ -33,8 +36,13 @@ from openedx.core.djangoapps.credit.models import (
|
||||
CreditEligibility
|
||||
)
|
||||
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_ECOMMERCE_WORKER = 'test_worker'
|
||||
|
||||
|
||||
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
|
||||
@@ -64,6 +72,86 @@ class CreditApiTestBase(ModuleStoreTestCase):
|
||||
}
|
||||
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):
|
||||
super(CreditApiTestBase, self).setUp()
|
||||
self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course")
|
||||
@@ -86,6 +174,15 @@ class CreditApiTestBase(ModuleStoreTestCase):
|
||||
|
||||
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')
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
|
||||
@@ -394,10 +491,20 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade")
|
||||
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):
|
||||
""" Test the credit requirements, eligibility notification, email
|
||||
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
|
||||
self.add_credit_course()
|
||||
CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
|
||||
@@ -435,7 +542,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key))
|
||||
|
||||
# Satisfy the other requirement
|
||||
with self.assertNumQueries(15):
|
||||
with self.assertNumQueries(19):
|
||||
api.set_credit_requirement_status(
|
||||
"bob",
|
||||
self.course_key,
|
||||
@@ -448,7 +555,10 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
|
||||
# Credit eligibility email should be sent
|
||||
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
|
||||
email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access
|
||||
@@ -471,11 +581,22 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
image_id = email_image.get('Content-ID', '')[1:-1]
|
||||
self.assertIsNotNone(image_id)
|
||||
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
|
||||
# requirement again to trigger eligibility notification
|
||||
CreditEligibility.objects.all().delete()
|
||||
with self.assertNumQueries(13):
|
||||
with self.assertNumQueries(15):
|
||||
api.set_credit_requirement_status(
|
||||
"bob",
|
||||
self.course_key,
|
||||
@@ -551,6 +672,87 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
self.assertEqual(len(req_status), 1)
|
||||
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')
|
||||
@ddt.ddt
|
||||
@@ -879,3 +1081,126 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
|
||||
"""Check the user's credit status. """
|
||||
statuses = api.get_credit_requests_for_user(self.USER_INFO["username"])
|
||||
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), [])
|
||||
|
||||
Reference in New Issue
Block a user