Commit 990a8cbf by Harry Rein Committed by GitHub

Merge pull request #16008 from edx/HarryRein/LEARNER-2307-course-goal-messaging

LEARNER-2307: Course Goal Messaging
parents bdb078d0 bc76ffe5
......@@ -156,7 +156,7 @@ class TestSaveSubsToStore(SharedModuleStoreTestCase):
def test_save_unjsonable_subs_to_store(self):
"""
Assures that subs, that can't be dumped, can't be found later.
Ensures that subs, that can't be dumped, can't be found later.
"""
with self.assertRaises(NotFoundError):
contentstore().find(self.content_location_unjsonable)
......
"""
Course Goals Python API
"""
from enum import Enum
from opaque_keys.edx.keys import CourseKey
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import Text
from .models import CourseGoal
def add_course_goal(user, course_id, goal_key):
"""
Add a new course goal for the provided user and course.
Arguments:
user: The user that is setting the goal
course_id (string): The id for the course the goal refers to
goal_key (string): The goal key that maps to one of the
enumerated goal keys from CourseGoalOption.
"""
# Create and save a new course goal
course_key = CourseKey.from_string(str(course_id))
new_goal = CourseGoal(user=user, course_key=course_key, goal_key=goal_key)
new_goal.save()
def get_course_goal(user, course_key):
"""
Given a user and a course_key, return their course goal.
If a course goal does not exist, returns None.
"""
course_goals = CourseGoal.objects.filter(user=user, course_key=course_key)
return course_goals[0] if course_goals else None
def remove_course_goal(user, course_key):
"""
Given a user and a course_key, remove the course goal.
"""
course_goal = get_course_goal(user, course_key)
if course_goal:
course_goal.delete()
class CourseGoalOption(Enum):
"""
Types of goals that a user can select.
These options are set to a string goal key so that they can be
referenced elsewhere in the code when necessary.
"""
CERTIFY = 'certify'
COMPLETE = 'complete'
EXPLORE = 'explore'
UNSURE = 'unsure'
@classmethod
def get_course_goal_keys(self):
return [key.value for key in self]
def get_goal_text(goal_option):
"""
This function is used to translate the course goal option into
a translated, user-facing string to be used to represent that
particular goal.
"""
return {
CourseGoalOption.CERTIFY.value: Text(_('Earn a certificate')),
CourseGoalOption.COMPLETE.value: Text(_('Complete the course')),
CourseGoalOption.EXPLORE.value: Text(_('Explore the course')),
CourseGoalOption.UNSURE.value: Text(_('Not sure yet')),
}[goal_option]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import openedx.core.djangoapps.xmodule_django.models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CourseGoal',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('course_key', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
('goal_key', models.CharField(default=b'unsure', max_length=100, choices=[(b'certify', 'Earn a certificate.'), (b'complete', 'Complete the course.'), (b'explore', 'Explore the course.'), (b'unsure', 'Not sure yet.')])),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='coursegoal',
unique_together=set([('user', 'course_key')]),
),
]
"""
Course Goals Models
"""
from django.contrib.auth.models import User
from django.db import models
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
class CourseGoal(models.Model):
"""
Represents a course goal set by a user on the course home page.
The goal_key represents the goal key that maps to a translated
string through using the CourseGoalOption class.
"""
GOAL_KEY_CHOICES = (
('certify', 'Earn a certificate.'),
('complete', 'Complete the course.'),
('explore', 'Explore the course.'),
('unsure', 'Not sure yet.'),
)
user = models.ForeignKey(User, blank=False)
course_key = CourseKeyField(max_length=255, db_index=True)
goal_key = models.CharField(max_length=100, choices=GOAL_KEY_CHOICES, default='unsure')
def __unicode__(self):
return 'CourseGoal: {user} set goal to {goal} for course {course}'.format(
user=self.user.username,
course=self.course_key,
goal_key=self.goal_key,
)
class Meta:
unique_together = ("user", "course_key")
"""
Course Goals Signals
"""
from django.db.models.signals import post_save
from django.dispatch import receiver
from eventtracking import tracker
from .models import CourseGoal
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goal_event")
def emit_course_goal_event(sender, instance, **kwargs):
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
tracker.emit(
name,
{
'goal_key': instance.goal_key,
}
)
"""
Unit tests for course_goals.api methods.
"""
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from lms.djangoapps.course_goals.models import CourseGoal
from rest_framework.test import APIClient
from student.models import CourseEnrollment
from track.tests import EventTrackingTestCase
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
TEST_PASSWORD = 'test'
class TestCourseGoalsAPI(EventTrackingTestCase, SharedModuleStoreTestCase):
"""
Testing the Course Goals API.
"""
def setUp(self):
# Create a course with a verified track
super(TestCourseGoalsAPI, self).setUp()
self.course = CourseFactory.create(emit_signals=True)
self.user = User.objects.create_user('john', 'lennon@thebeatles.com', 'password')
CourseEnrollment.enroll(self.user, self.course.id)
self.client = APIClient(enforce_csrf_checks=True)
self.client.login(username=self.user.username, password=self.user.password)
self.client.force_authenticate(user=self.user)
self.apiUrl = reverse('course_goals_api:v0:course_goal-list')
def test_add_valid_goal(self):
""" Ensures a correctly formatted post succeeds. """
response = self.post_course_goal(valid=True)
self.assert_events_emitted()
self.assertEqual(response.status_code, 201)
self.assertEqual(len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)), 1)
def test_add_invalid_goal(self):
""" Ensures a correctly formatted post succeeds. """
response = self.post_course_goal(valid=False)
self.assertEqual(response.status_code, 400)
self.assertEqual(len(CourseGoal.objects.filter(user=self.user, course_key=self.course.id)), 0)
def post_course_goal(self, valid=True, goal_key='certify'):
"""
Sends a post request to set a course goal and returns the response.
"""
goal_key = goal_key if valid else 'invalid'
response = self.client.post(
self.apiUrl,
{
'goal_key': goal_key,
'course_key': self.course.id,
'user': self.user.username,
},
)
return response
"""
Course Goals URLs
"""
from django.conf.urls import include, patterns, url
from rest_framework import routers
from .views import CourseGoalViewSet
router = routers.DefaultRouter()
router.register(r'course_goals', CourseGoalViewSet, base_name='course_goal')
urlpatterns = patterns(
'',
url(r'^v0/', include(router.urls, namespace='v0')),
)
"""
Course Goals Views - includes REST API
"""
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from edx_rest_framework_extensions.authentication import JwtAuthentication
from eventtracking import tracker
from opaque_keys.edx.keys import CourseKey
from openedx.core.lib.api.permissions import IsStaffOrOwner
from rest_framework import permissions, serializers, viewsets
from rest_framework.authentication import SessionAuthentication
from .api import CourseGoalOption
from .models import CourseGoal
User = get_user_model()
class CourseGoalSerializer(serializers.ModelSerializer):
"""
Serializes CourseGoal models.
"""
user = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all())
class Meta:
model = CourseGoal
fields = ('user', 'course_key', 'goal_key')
def validate_goal_key(self, value):
"""
Ensure that the goal_key is valid.
"""
if value not in CourseGoalOption.get_course_goal_keys():
raise serializers.ValidationError(
'Provided goal key, {goal_key}, is not a valid goal key (options= {goal_options}).'.format(
goal_key=value,
goal_options=[option.value for option in CourseGoalOption],
)
)
return value
def validate_course_key(self, value):
"""
Ensure that the course_key is valid.
"""
course_key = CourseKey.from_string(value)
if not course_key:
raise serializers.ValidationError(
'Provided course_key ({course_key}) does not map to a course.'.format(
course_key=course_key
)
)
return course_key
class CourseGoalViewSet(viewsets.ModelViewSet):
"""
API calls to create and retrieve a course goal.
**Use Case**
* Create a new goal for a user.
Http400 is returned if the format of the request is not correct,
the course_id or goal is invalid or cannot be found.
* Retrieve goal for a user and a particular course.
Http400 is returned if the format of the request is not correct,
or the course_id is invalid or cannot be found.
**Example Requests**
GET /api/course_goals/v0/course_goals/
POST /api/course_goals/v0/course_goals/
Request data: {"course_key": <course-key>, "goal_key": "<goal-key>", "user": "<username>"}
"""
authentication_classes = (JwtAuthentication, SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,)
queryset = CourseGoal.objects.all()
serializer_class = CourseGoalSerializer
@receiver(post_save, sender=CourseGoal, dispatch_uid="emit_course_goals_event")
def emit_course_goal_event(sender, instance, **kwargs):
name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated'
tracker.emit(
name,
{
'goal_key': instance.goal_key,
}
)
......@@ -401,6 +401,9 @@ FEATURES = {
# Whether the bulk enrollment view is enabled.
'ENABLE_BULK_ENROLLMENT_VIEW': False,
# Whether course goals is enabled.
'ENABLE_COURSE_GOALS': True,
}
# Settings for the course reviews tool template and identification key, set either to None to disable course reviews
......@@ -2245,6 +2248,9 @@ INSTALLED_APPS = [
'openedx.core.djangoapps.waffle_utils',
'openedx.core.djangoapps.schedules.apps.SchedulesConfig',
# Course Goals
'lms.djangoapps.course_goals',
# Features
'openedx.features.course_bookmarks',
'openedx.features.course_experience',
......
......@@ -15,11 +15,12 @@
}
.message-content {
@include margin(0, 0, $baseline, $baseline);
position: relative;
border: 1px solid $lms-border-color;
margin: 0 $baseline $baseline/2;
padding: $baseline/2 $baseline;
padding: $baseline;
border-radius: $baseline/4;
width: calc(100% - 90px);
@media (max-width: $grid-breakpoints-md) {
width: 100%;
......@@ -30,7 +31,7 @@
&::before {
@include left(0);
bottom: 35%;
top: 25px;
border: solid transparent;
height: 0;
width: 0;
......@@ -58,13 +59,49 @@
.message-header {
font-weight: $font-semibold;
margin-bottom: $baseline/4;
margin-bottom: $baseline/2;
width: calc(100% - 40px)
}
a {
font-weight: $font-semibold;
text-decoration: underline;
}
.dismiss {
@include right($baseline/4);
top: $baseline/4;
position: absolute;
cursor: pointer;
color: $black-t3;
&:hover {
color: $black-t2;
}
}
// Course Goal Styling
.goal-options-container {
margin-top: $baseline;
text-align: center;
.goal-option {
text-decoration: none;
font-size: font-size(x-small);
padding: $baseline/2;
&.dismissible {
@include right($baseline/4);
position: absolute;
top: $baseline/2;
font-size: font-size(small);
color: $uxpl-blue-base;
cursor: pointer;
&:hover {
color: $black-t2;
}
}
}
}
}
}
......
......@@ -50,7 +50,7 @@ site_status_msg = get_site_status_msg(course_id)
</%block>
% if uses_bootstrap:
<header class="navigation-container header-global ${"slim" if course else ""}">
<header class="navigation-container header-global ${'slim' if course else ''}">
<nav class="navbar navbar-expand-lg navbar-light">
<%include file="bootstrap/navbar-logo-header.html" args="online_help_token=online_help_token"/>
<button class="navbar-toggler navbar-toggler-right mt-2" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
......
......@@ -829,6 +829,11 @@ urlpatterns += (
url(r'^commerce/', include('commerce.urls', namespace='commerce')),
)
# Course goals
urlpatterns += (
url(r'^api/course_goals/', include('lms.djangoapps.course_goals.urls', namespace='course_goals_api')),
)
# Embargo
if settings.FEATURES.get('EMBARGO'):
urlpatterns += (
......
......@@ -166,4 +166,5 @@ class IsStaffOrOwner(permissions.BasePermission):
return user.is_staff \
or (user.username == request.GET.get('username')) \
or (user.username == getattr(request, 'data', {}).get('username')) \
or (user.username == getattr(request, 'data', {}).get('user')) \
or (user.username == getattr(view, 'kwargs', {}).get('username'))
......@@ -16,15 +16,18 @@ COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'course_outli
# Waffle flag to enable a single unified "Course" tab.
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
# Waffle flag to enable the sock on the footer of the home and courseware pages
# Waffle flag to enable the sock on the footer of the home and courseware pages.
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock')
# Waffle flag to let learners access a course before its start date
# Waffle flag to let learners access a course before its start date.
COURSE_PRE_START_ACCESS_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_start_access')
# Waffle flag to enable a review page link from the unified home page
# Waffle flag to enable a review page link from the unified home page.
SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_tool')
# Waffle flag to enable the setting of course goals.
ENABLE_COURSE_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_course_goals')
SHOW_UPGRADE_MSG_ON_COURSE_HOME = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_upgrade_msg_on_course_home')
# Waffle flag to switch between the 'welcome message' and 'latest update' on the course home page.
......
/* globals gettext */
export class CourseGoals { // eslint-disable-line import/prefer-default-export
constructor(options) {
$('.goal-option').click((e) => {
const goalKey = $(e.target).data().choice;
$.ajax({
method: 'POST',
url: options.goalApiUrl,
headers: { 'X-CSRFToken': $.cookie('csrftoken') },
data: {
goal_key: goalKey,
course_key: options.courseId,
user: options.username,
},
dataType: 'json',
success: () => {
// LEARNER-2522 will address the success message
const successMsg = gettext('Thank you for setting your course goal!');
// xss-lint: disable=javascript-jquery-html
$('.message-content').html(`<div class="success-message">${successMsg}</div>`);
},
error: () => {
// LEARNER-2522 will address the error message
const errorMsg = gettext('There was an error in setting your goal, please reload the page and try again.'); // eslint-disable-line max-len
// xss-lint: disable=javascript-jquery-html
$('.message-content').html(`<div class="error-message"> ${errorMsg} </div>`);
},
});
});
// Allow goal selection with an enter press for accessibility purposes
$('.goal-option').keyup((e) => {
if (e.which === 13) {
$(e.target).trigger('click');
}
});
}
}
......@@ -30,6 +30,18 @@ export class CourseHome { // eslint-disable-line import/prefer-default-export
);
});
// Dismissibility for in course messages
$(document.body).on('click', '.course-message .dismiss', (event) => {
$(event.target).closest('.course-message').hide();
});
// Allow dismiss on enter press for accessibility purposes
$(document.body).on('keyup', '.course-message .dismiss', (event) => {
if (event.which === 13) {
$(event.target).trigger('click');
}
});
$(document).ready(() => {
this.configureUpgradeMessage();
});
......
......@@ -19,7 +19,7 @@ export class CourseSock { // eslint-disable-line import/prefer-default-export
const startFixed = $verificationSock.offset().top + 320;
const endFixed = (startFixed + $verificationSock.height()) - 220;
// Assure update button stays in sock even when max-width is exceeded
// Ensure update button stays in sock even when max-width is exceeded
const distLeft = ($verificationSock.offset().left + $verificationSock.width())
- ($upgradeToVerifiedButton.width() + 22);
......
......@@ -5,6 +5,8 @@
<%!
from django.utils.translation import get_language_bidi
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import CourseHomeMessages
%>
......@@ -17,14 +19,22 @@ is_rtl = get_language_bidi()
% for message in course_home_messages:
<div class="course-message grid-manual">
% if not is_rtl:
<img class="message-author col col-2" src="${static.url(image_src)}"/>
<img class="message-author" alt="${_('Course message author')}" role="none" src="${static.url(image_src)}"/>
% endif
<div class="message-content col col-9">
<div class="message-content">
${HTML(message.message_html)}
</div>
% if is_rtl:
<img class="message-author col col-2" src="${static.url(image_src)}"/>
<img class="message-author" alt="${_('Course message author')}" role="none" src="${static.url(image_src)}"/>
% endif
</div>
% endfor
% endif
<%static:webpack entry="CourseGoals">
new CourseGoals({
goalApiUrl: "${goal_api_url | n, js_escaped_string}",
courseId: "${course_id | n, js_escaped_string}",
username: "${username | n, js_escaped_string}",
});
</%static:webpack>
......@@ -16,6 +16,7 @@ from waffle.testutils import override_flag
from commerce.models import CommerceConfiguration
from commerce.utils import EcommerceService
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
from course_modes.models import CourseMode
from courseware.tests.factories import StaffFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
......@@ -25,14 +26,14 @@ from openedx.features.course_experience import (
UNIFIED_COURSE_TAB_FLAG
)
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from util.date_utils import strftime_localized
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from .helpers import add_course_mode
from .test_course_updates import create_course_update, remove_course_updates
from ... import COURSE_PRE_START_ACCESS_FLAG
from ... import COURSE_PRE_START_ACCESS_FLAG, ENABLE_COURSE_GOALS
TEST_PASSWORD = 'test'
TEST_CHAPTER_NAME = 'Test Chapter'
......@@ -43,6 +44,8 @@ TEST_COURSE_HOME_MESSAGE = 'course-message'
TEST_COURSE_HOME_MESSAGE_ANONYMOUS = '/login'
TEST_COURSE_HOME_MESSAGE_UNENROLLED = 'Enroll now'
TEST_COURSE_HOME_MESSAGE_PRE_START = 'Course starts in'
TEST_COURSE_GOAL_OPTIONS = 'goal-options-container'
COURSE_GOAL_DISMISS_OPTION = 'unsure'
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
......@@ -170,7 +173,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
with self.assertNumQueries(41, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(44, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
......@@ -375,11 +378,13 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
# Verify that enrolled users are not shown a message when enrolled and course has begun
# Verify that enrolled users are not shown any state warning message when enrolled and course has begun.
CourseEnrollment.enroll(user, self.course.id)
url = course_home_url(self.course)
response = self.client.get(url)
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE)
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
# Verify that enrolled users are shown 'days until start' message before start date
future_course = self.create_future_course()
......@@ -389,6 +394,50 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
@override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True)
@override_waffle_flag(ENABLE_COURSE_GOALS, active=True)
def test_course_goals(self):
"""
Ensure that the following five use cases work as expected.
1) Unenrolled users are not shown the set course goal message.
2) Enrolled users are shown the set course goal message if they have not yet set a course goal.
3) Enrolled users are not shown the set course goal message if they have set a course goal.
4) Enrolled and verified users are not shown the set course goal message.
5) Enrolled users are not shown the set course goal message in a course that cannot be verified.
"""
# Create a course with a verified track.
verifiable_course = CourseFactory.create()
add_course_mode(verifiable_course, upgrade_deadline_expired=False)
# Verify that unenrolled users are not shown the set course goal message.
user = self.create_user_for_course(verifiable_course, CourseUserType.UNENROLLED)
response = self.client.get(course_home_url(verifiable_course))
self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
# Verify that enrolled users are shown the set course goal message in a verified course.
CourseEnrollment.enroll(user, verifiable_course.id)
response = self.client.get(course_home_url(verifiable_course))
self.assertContains(response, TEST_COURSE_GOAL_OPTIONS)
# Verify that enrolled users that have set a course goal are not shown the set course goal message.
add_course_goal(user, verifiable_course.id, COURSE_GOAL_DISMISS_OPTION)
response = self.client.get(course_home_url(verifiable_course))
self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
# Verify that enrolled and verified users are not shown the set course goal message.
remove_course_goal(user, verifiable_course.id)
CourseEnrollment.enroll(user, verifiable_course.id, CourseMode.VERIFIED)
response = self.client.get(course_home_url(verifiable_course))
self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
# Verify that enrolled users are not shown the set course goal message in an audit only course.
audit_only_course = CourseFactory.create()
CourseEnrollment.enroll(user, audit_only_course.id)
response = self.client.get(course_home_url(audit_only_course))
self.assertNotContains(response, TEST_COURSE_GOAL_OPTIONS)
class CourseHomeFragmentViewTests(ModuleStoreTestCase):
CREATE_USER = False
......
......@@ -56,7 +56,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_standard_course(self):
"""
Assure that a course that cannot be verified does
Ensure that a course that cannot be verified does
not have a visible verification sock.
"""
response = self.client.get(course_home_url(self.standard_course))
......@@ -65,7 +65,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course(self):
"""
Assure that a course that can be verified has a
Ensure that a course that can be verified has a
visible verification sock.
"""
response = self.client.get(course_home_url(self.verified_course))
......@@ -74,7 +74,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course_updated_expired(self):
"""
Assure that a course that has an expired upgrade
Ensure that a course that has an expired upgrade
date does not display the verification sock.
"""
response = self.client.get(course_home_url(self.verified_course_update_expired))
......@@ -83,7 +83,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
@override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course_user_already_upgraded(self):
"""
Assure that a user that has already upgraded to a
Ensure that a user that has already upgraded to a
verified status cannot see the verification sock.
"""
response = self.client.get(course_home_url(self.verified_course_already_enrolled))
......
......@@ -23,6 +23,7 @@ var wpconfig = {
StudioIndex: './cms/static/js/features_jsx/studio/index.jsx',
// Features
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js',
CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js',
CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js',
......
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