Add message for setting course goal.

LEARNER-2307
This commit is contained in:
Harry Rein
2017-09-11 10:43:24 -04:00
committed by Robert Raposa
parent a58df6c0fa
commit bc76ffe5dc
25 changed files with 644 additions and 73 deletions

View File

@@ -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'))

View File

@@ -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.

View File

@@ -0,0 +1,40 @@
/* 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');
}
});
}
}

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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))

View File

@@ -1,22 +1,30 @@
"""
View logic for handling course messages.
"""
from babel.dates import format_date, format_timedelta
import math
from datetime import datetime
from courseware.courses import get_course_with_access
from babel.dates import format_date, format_timedelta
from django.conf import settings
from django.contrib import auth
from django.template.loader import render_to_string
from django.utils.http import urlquote_plus
from django.utils.timezone import UTC
from django.utils.translation import get_language, to_locale
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import Text, HTML
from django.utils.translation import get_language, to_locale
from opaque_keys.edx.keys import CourseKey
from rest_framework.reverse import reverse
from web_fragments.fragment import Fragment
from course_modes.models import CourseMode
from courseware.courses import get_course_with_access
from lms.djangoapps.course_goals.api import CourseGoalOption, get_course_goal, get_goal_text
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import CourseHomeMessages
from student.models import CourseEnrollment
from .. import ENABLE_COURSE_GOALS
class CourseHomeMessageFragmentView(EdxFragmentView):
@@ -55,69 +63,140 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
}
# Register the course home messages to be loaded on the page
self.register_course_home_messages(request, course, user_access, course_start_data)
_register_course_home_messages(request, course_id, user_access, course_start_data)
# Grab the relevant messages
course_home_messages = list(CourseHomeMessages.user_messages(request))
# Return None if user is enrolled and course has begun
if user_access['is_enrolled'] and already_started:
return None
# Pass in the url used to set a course goal
goal_api_url = reverse('course_goals_api:v0:course_goal-list', request=request)
# Grab the logo
image_src = "course_experience/images/home_message_author.png"
context = {
'course_home_messages': course_home_messages,
'goal_api_url': goal_api_url,
'image_src': image_src,
'course_id': course_id,
'username': request.user.username,
}
html = render_to_string('course_experience/course-messages-fragment.html', context)
return Fragment(html)
@staticmethod
def register_course_home_messages(request, course, user_access, course_start_data):
"""
Register messages to be shown in the course home content page.
"""
if user_access['is_anonymous']:
CourseHomeMessages.register_info_message(
request,
Text(_(
" {sign_in_link} or {register_link} and then enroll in this course."
)).format(
sign_in_link=HTML("<a href='/login?next={current_url}'>{sign_in_label}</a>").format(
sign_in_label=_("Sign in"),
current_url=urlquote_plus(request.path),
def _register_course_home_messages(request, course_id, user_access, course_start_data):
"""
Register messages to be shown in the course home content page.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key)
if user_access['is_anonymous']:
CourseHomeMessages.register_info_message(
request,
Text(_(
" {sign_in_link} or {register_link} and then enroll in this course."
)).format(
sign_in_link=HTML("<a href='/login?next={current_url}'>{sign_in_label}</a>").format(
sign_in_label=_("Sign in"),
current_url=urlquote_plus(request.path),
),
register_link=HTML("<a href='/register?next={current_url}'>{register_label}</a>").format(
register_label=_("register"),
current_url=urlquote_plus(request.path),
)
),
title=Text(_('You must be enrolled in the course to see course content.'))
)
if not user_access['is_anonymous'] and not user_access['is_staff'] and not user_access['is_enrolled']:
CourseHomeMessages.register_info_message(
request,
Text(_(
"{open_enroll_link} Enroll now{close_enroll_link} to access the full course."
)).format(
open_enroll_link='',
close_enroll_link=''
),
title=Text(_('Welcome to {course_display_name}')).format(
course_display_name=course.display_name
)
)
if user_access['is_enrolled'] and not course_start_data['already_started']:
CourseHomeMessages.register_info_message(
request,
Text(_(
"Don't forget to add a calendar reminder!"
)),
title=Text(_("Course starts in {days_until_start_string} on {course_start_date}.")).format(
days_until_start_string=course_start_data['days_until_start_string'],
course_start_date=course_start_data['course_start_date']
)
)
# Only show the set course goal message for enrolled, unverified
# users that have not yet set a goal in a course that allows for
# verified statuses.
has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(unicode(course.id)))
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
user_goal = get_course_goal(auth.get_user(request), course_key) if not request.user.is_anonymous() else None
if user_access['is_enrolled'] and has_verified_mode and not is_already_verified and not user_goal \
and ENABLE_COURSE_GOALS.is_enabled(course_key) and settings.FEATURES.get('ENABLE_COURSE_GOALS'):
goal_choices_html = Text(_(
'To start, set a course goal by selecting the option below that best describes '
'your learning plan. {goal_options_container}'
)).format(
goal_options_container=HTML('<div class="row goal-options-container">')
)
# Add the dismissible option for users that are unsure of their goal
goal_choices_html += Text(
'{initial_tag}{choice}{closing_tag}'
).format(
initial_tag=HTML(
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option dismissible" '
'data-choice="{goal_key}">'
).format(
goal_key=CourseGoalOption.UNSURE.value,
aria_label_choice=Text(_("Set goal to: {choice}")).format(
choice=get_goal_text(CourseGoalOption.UNSURE.value)
),
),
choice=Text(_('{choice}')).format(
choice=get_goal_text(CourseGoalOption.UNSURE.value),
),
closing_tag=HTML('</div>'),
)
# Add the option to set a goal to earn a certificate,
# complete the course or explore the course
goal_options = [CourseGoalOption.CERTIFY.value, CourseGoalOption.COMPLETE.value, CourseGoalOption.EXPLORE.value]
for goal_key in goal_options:
goal_text = get_goal_text(goal_key)
goal_choices_html += HTML(
'{initial_tag}{goal_text}{closing_tag}'
).format(
initial_tag=HTML(
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option {col_sel} btn" '
'data-choice="{goal_key}">'
).format(
goal_key=goal_key,
aria_label_choice=Text(_("Set goal to: {goal_text}")).format(
goal_text=Text(_(goal_text))
),
register_link=HTML("<a href='/register?next={current_url}'>{register_label}</a>").format(
register_label=_("register"),
current_url=urlquote_plus(request.path),
)
col_sel='col-' + str(int(math.floor(12 / len(goal_options))))
),
title='You must be enrolled in the course to see course content.'
goal_text=goal_text,
closing_tag=HTML('</div>')
)
if not user_access['is_anonymous'] and not user_access['is_staff'] and not user_access['is_enrolled']:
CourseHomeMessages.register_info_message(
request,
Text(_(
"{open_enroll_link} Enroll now{close_enroll_link} to access the full course."
)).format(
open_enroll_link='',
close_enroll_link=''
),
title=Text('Welcome to {course_display_name}').format(
course_display_name=course.display_name
)
)
if user_access['is_enrolled'] and not course_start_data['already_started']:
CourseHomeMessages.register_info_message(
request,
Text(_(
"Don't forget to add a calendar reminder!"
)),
title=Text("Course starts in {days_until_start_string} on {course_start_date}.").format(
days_until_start_string=course_start_data['days_until_start_string'],
course_start_date=course_start_data['course_start_date']
)
CourseHomeMessages.register_info_message(
request,
HTML('{goal_choices_html}{closing_tag}').format(
goal_choices_html=goal_choices_html,
closing_tag=HTML('</div>')
),
title=Text(_('Welcome to {course_display_name}')).format(
course_display_name=course.display_name
)
)