Skip to content
Snippets Groups Projects
Commit 3f22ce30 authored by sandroroux's avatar sandroroux
Browse files

These changes add a resume button to course cards on the student dashboard.

parent 50574ea9
No related merge requests found
......@@ -3,10 +3,12 @@ Test the student dashboard view.
"""
import itertools
import json
import re
import unittest
from datetime import timedelta
import ddt
from completion.test_utils import submit_completions_for_testing, CompletionWaffleTestMixin
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase
......@@ -241,7 +243,7 @@ class LogoutTests(TestCase):
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, CompletionWaffleTestMixin):
"""
Tests for the student dashboard.
"""
......@@ -603,6 +605,228 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
response = self.client.get(self.path)
self.assertEqual(pq(response.content)(self.EMAIL_SETTINGS_ELEMENT_ID).length, 0)
@staticmethod
def _remove_whitespace_from_html_string(html):
return ''.join(html.split())
@staticmethod
def _pull_course_run_from_course_key(course_key_string):
search_results = re.search(r'Run_[0-9]+$', course_key_string)
assert search_results
course_run_string = search_results.group(0).replace('_', ' ')
return course_run_string
@staticmethod
def _get_html_for_view_course_button(course_key_string, course_run_string):
return '''
<a href="/courses/{course_key}/course/"
class="enter-course "
data-course-key="{course_key}">
View Course
<span class="sr">
&nbsp;{course_run}
</span>
</a>
'''.format(course_key=course_key_string, course_run=course_run_string)
@staticmethod
def _get_html_for_resume_course_button(course_key_string, resume_block_key_string, course_run_string):
return '''
<a href="/courses/{course_key}/jump_to/{url_to_block}"
class="enter-course "
data-course-key="{course_key}">
Resume Course
<span class="sr">
&nbsp;{course_run}
</span>
</a>
'''.format(
course_key=course_key_string,
url_to_block=resume_block_key_string,
course_run=course_run_string
)
def test_view_course_appears_on_dashboard(self):
"""
When a course doesn't have completion data, its course card should
display a "View Course" button.
"""
self.override_waffle_switch(True)
course = CourseFactory.create()
CourseEnrollmentFactory.create(
user=self.user,
course_id=course.id
)
response = self.client.get(reverse('dashboard'))
course_key_string = str(course.id)
# No completion data means there's no block from which to resume.
resume_block_key_string = ''
course_run_string = self._pull_course_run_from_course_key(course_key_string)
view_button_html = self._get_html_for_view_course_button(
course_key_string,
course_run_string
)
resume_button_html = self._get_html_for_resume_course_button(
course_key_string,
resume_block_key_string,
course_run_string
)
view_button_html = self._remove_whitespace_from_html_string(view_button_html)
resume_button_html = self._remove_whitespace_from_html_string(resume_button_html)
dashboard_html = self._remove_whitespace_from_html_string(response.content)
self.assertIn(
view_button_html,
dashboard_html
)
self.assertNotIn(
resume_button_html,
dashboard_html
)
def test_resume_course_appears_on_dashboard(self):
"""
When a course has completion data, its course card should display a
"Resume Course" button.
"""
self.override_waffle_switch(True)
course = CourseFactory.create()
CourseEnrollmentFactory.create(
user=self.user,
course_id=course.id
)
course_key = course.id
block_keys = [
course_key.make_usage_key('video', unicode(number))
for number in xrange(5)
]
submit_completions_for_testing(self.user, course_key, block_keys)
with patch('completion.utilities.visual_progress_enabled', return_value=True):
response = self.client.get(reverse('dashboard'))
course_key_string = str(course_key)
resume_block_key_string = str(block_keys[-1])
course_run_string = self._pull_course_run_from_course_key(course_key_string)
view_button_html = self._get_html_for_view_course_button(
course_key_string,
course_run_string
)
resume_button_html = self._get_html_for_resume_course_button(
course_key_string,
resume_block_key_string,
course_run_string
)
view_button_html = self._remove_whitespace_from_html_string(view_button_html)
resume_button_html = self._remove_whitespace_from_html_string(resume_button_html)
dashboard_html = self._remove_whitespace_from_html_string(response.content)
self.assertIn(
resume_button_html,
dashboard_html
)
self.assertNotIn(
view_button_html,
dashboard_html
)
def test_dashboard_with_resume_buttons_and_view_buttons(self):
'''
The Test creates a four-course-card dashboard. The user completes course
blocks in the even-numbered course cards. The test checks that courses
with completion data have course cards with "Resume Course" buttons;
those without have "View Course" buttons.
'''
self.override_waffle_switch(True)
isEven = lambda n: n % 2 == 0
num_course_cards = 4
html_for_view_buttons = []
html_for_resume_buttons = []
for i in range(num_course_cards):
course = CourseFactory.create()
course_enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=course.id
)
course_key = course_enrollment.course_id
course_key_string = str(course_key)
last_completed_block_string = ''
course_run_string = self._pull_course_run_from_course_key(
course_key_string)
# Submit completed course blocks in even-numbered courses.
if isEven(i):
block_keys = [
course_key.make_usage_key('video', unicode(number))
for number in xrange(5)
]
last_completed_block_string = str(block_keys[-1])
submit_completions_for_testing(self.user, course_key, block_keys)
html_for_view_buttons.append(
self._get_html_for_view_course_button(
course_key_string,
course_run_string
)
)
html_for_resume_buttons.append(
self._get_html_for_resume_course_button(
course_key_string,
last_completed_block_string,
course_run_string
)
)
with patch('completion.utilities.visual_progress_enabled', return_value=True):
response = self.client.get(reverse('dashboard'))
html_for_view_buttons = [
self._remove_whitespace_from_html_string(button)
for button in html_for_view_buttons
]
html_for_resume_buttons = [
self._remove_whitespace_from_html_string(button)
for button in html_for_resume_buttons
]
dashboard_html = self._remove_whitespace_from_html_string(response.content)
for i in range(num_course_cards):
expected_button = None
unexpected_button = None
if isEven(i):
expected_button = html_for_resume_buttons[i]
unexpected_button = html_for_view_buttons[i]
else:
expected_button = html_for_view_buttons[i]
unexpected_button = html_for_resume_buttons[i]
self.assertIn(
expected_button,
dashboard_html
)
self.assertNotIn(
unexpected_button,
dashboard_html
)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(BRANCH_IO_KEY='test_key')
......
......@@ -6,6 +6,8 @@ import datetime
import logging
from collections import defaultdict
from completion.exceptions import UnavailableCompletionData
from completion.utilities import get_key_to_last_completed_course_block
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
......@@ -13,6 +15,7 @@ from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from six import text_type, iteritems
......@@ -453,6 +456,24 @@ def _credit_statuses(user, course_enrollments):
return statuses
def _get_urls_for_resume_buttons(user, enrollments):
'''
Checks whether a user has made progress in any of a list of enrollments.
'''
resume_button_urls = []
for enrollment in enrollments:
try:
block_key = get_key_to_last_completed_course_block(user, enrollment.course_id)
url_to_block = reverse(
'jump_to',
kwargs={'course_id': enrollment.course_id, 'location': block_key}
)
except UnavailableCompletionData:
url_to_block = ''
resume_button_urls.append(url_to_block)
return resume_button_urls
@login_required
@ensure_csrf_cookie
def student_dashboard(request):
......@@ -473,6 +494,7 @@ def student_dashboard(request):
return redirect(reverse('account_settings'))
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
enable_verified_certificates = configuration_helpers.get_value(
'ENABLE_VERIFIED_CERTIFICATES',
settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES')
......@@ -756,6 +778,15 @@ def student_dashboard(request):
'ecommerce_payment_page': ecommerce_service.payment_page_url(),
})
# Gather urls for course card resume buttons.
resume_button_urls = _get_urls_for_resume_buttons(user, course_enrollments)
# There must be enough urls for dashboard.html. Template creates course
# cards for "enrollments + entitlements".
resume_button_urls += ['' for entitlement in course_entitlements]
context.update({
'resume_button_urls': resume_button_urls
})
response = render_to_response('dashboard.html', context)
set_user_info_cookie(response, request)
return response
......@@ -185,8 +185,9 @@ from student.models import CourseEnrollment
related_programs = inverted_programs.get(unicode(entitlement.course_uuid if is_unfulfilled_entitlement else session_id))
show_consent_link = (session_id in consent_required_courses)
course_overview = enrollment.course_overview
resume_button_url = resume_button_urls[dashboard_index]
%>
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url' />
% endfor
</ul>
......
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name" expression_filter="h"/>
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name, resume_button_url" expression_filter="h"/>
<%!
import urllib
......@@ -172,11 +172,34 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% else:
<a class="enter-course-blocked archived" data-course-key="${enrollment.course_id}">${_('View Archived Course')}<span class="sr">&nbsp;${course_overview.display_name_with_default}</span></a>
% endif
% else:
% if not is_course_blocked:
<a href="${course_target}" class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}" data-course-key="${enrollment.course_id}">${_('View Course')}<span class="sr">&nbsp;${course_overview.display_name_with_default}</span></a>
% if resume_button_url != '':
<a href="${resume_button_url}"
class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}"
data-course-key="${enrollment.course_id}">
${_('Resume Course')}
<span class="sr">
&nbsp;${course_overview.display_name_with_default}
</span>
</a>
% elif not is_course_blocked:
<a href="${course_target}"
class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}"
data-course-key="${enrollment.course_id}">
${_('View Course')}
<span class="sr">
&nbsp;${course_overview.display_name_with_default}
</span>
</a>
% else:
<a class="enter-course-blocked" data-course-key="${enrollment.course_id}">${_('View Course')}<span class="sr">&nbsp;${course_overview.display_name_with_default}</span></a>
<a class="enter-course-blocked"
data-course-key="${enrollment.course_id}">
${_('View Course')}
<span class="sr">
&nbsp;${course_overview.display_name_with_default}
</span>
</a>
% endif
% endif
% endif
......
......@@ -5,7 +5,7 @@ Test models, managers, and validators.
from __future__ import absolute_import, division, print_function, unicode_literals
from completion import models, waffle
from completion.test_utils import CompletionWaffleTestMixin
from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing
from django.core.exceptions import ValidationError
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey, UsageKey
......@@ -200,34 +200,9 @@ class BatchCompletionMethodTests(CompletionWaffleTestMixin, TestCase):
self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904")
self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)]
self.submit_fake_completions()
def submit_fake_completions(self):
"""
Submit completions for given runtime, run at setup
"""
for idx, block_key in enumerate(self.block_keys[:3]):
models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.course_key,
block_key=block_key,
completion=1.0 - (0.2 * idx),
)
for idx, block_key in enumerate(self.block_keys[2:]): # Wrong user
models.BlockCompletion.objects.submit_completion(
user=self.other_user,
course_key=self.course_key,
block_key=block_key,
completion=0.9 - (0.2 * idx),
)
models.BlockCompletion.objects.submit_completion( # Wrong course
user=self.user,
course_key=self.other_course_key,
block_key=self.block_keys[4],
completion=0.75,
)
submit_completions_for_testing(self.user, self.course_key, self.block_keys[:3])
submit_completions_for_testing(self.other_user, self.course_key, self.block_keys[2:])
submit_completions_for_testing(self.user, self.other_course_key, [self.block_keys[4]])
def test_get_course_completions_missing_runs(self):
actual_completions = models.BlockCompletion.get_course_completions(self.user, self.course_key)
......
......@@ -56,7 +56,7 @@ git+https://github.com/cpennington/pylint-django@fix-field-inference-during-monk
enum34==1.1.6
edx-django-oauth2-provider==1.2.5
edx-django-sites-extensions==2.3.0
edx-completion==0.0.6
edx-completion==0.0.9
edx-enterprise==0.65.7
edx-milestones==0.1.13
edx-oauth2-provider==1.2.2
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment