Add "last completed block" to dropdown

This commit is contained in:
Gregory Martin
2018-02-12 13:45:16 -05:00
committed by Sanford Student
parent 335bf4b035
commit a5d8cbb8e9
7 changed files with 202 additions and 6 deletions

View File

@@ -8,10 +8,12 @@ import time
import six
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import NoReverseMatch, reverse
from django.dispatch import Signal
from django.utils.http import cookie_date
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
from student.models import CourseEnrollment
CREATE_LOGON_COOKIE = Signal(providing_args=['user', 'response'])
@@ -58,6 +60,8 @@ def set_logged_in_cookies(request, response, user):
"username": "test-user",
"header_urls": {
"account_settings": "https://example.com/account/settings",
"resume_block":
"https://example.com//courses/org.0/course_0/Run_0/jump_to/i4x://org.0/course_0/vertical/vertical_4"
"learner_profile": "https://example.com/u/test-user",
"logout": "https://example.com/logout"
}
@@ -165,6 +169,12 @@ def get_user_info_cookie_data(request):
except NoReverseMatch:
pass
# Add 'resume course' last completed block
try:
header_urls['resume_block'] = retrieve_last_sitewide_block_completed(user)
except User.DoesNotExist:
pass
# Convert relative URL paths to absolute URIs
for url_name, url_path in six.iteritems(header_urls):
header_urls[url_name] = request.build_absolute_uri(url_path)

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import RequestFactory
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
from student.cookies import get_user_info_cookie_data
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
@@ -26,6 +27,7 @@ class CookieTests(SharedModuleStoreTestCase):
def _get_expected_header_urls(self, request):
expected_header_urls = {
'logout': reverse('logout'),
'resume_block': retrieve_last_sitewide_block_completed(self.user.username)
}
# Studio (CMS) does not have the URLs below

View File

@@ -39,7 +39,7 @@ from util.milestones_helpers import (get_course_milestones,
from util.testing import UrlResetMixin
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
PASSWORD = 'test'
@@ -704,7 +704,11 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
course_key = course.id
block_keys = [
course_key.make_usage_key('video', unicode(number))
ItemFactory.create(
category='video',
parent_location=course.location,
display_name='Video {0}'.format(unicode(number))
).location
for number in xrange(5)
]
@@ -773,7 +777,11 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
# Submit completed course blocks in even-numbered courses.
if isEven(i):
block_keys = [
course_key.make_usage_key('video', unicode(number))
ItemFactory.create(
category='video',
parent_location=course.location,
display_name='Video {0}'.format(unicode(number))
).location
for number in xrange(5)
]
last_completed_block_string = str(block_keys[-1])

View File

@@ -12,11 +12,14 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
%>
<%
profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
username = self.real_user.username
resume_block = retrieve_last_sitewide_block_completed(username)
%>
@@ -32,7 +35,11 @@ from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_
<span class="fa fa-caret-down" aria-hidden="true"></span>
</div>
<div class="dropdown-user-menu hidden" aria-label=${_("More Options")} role="menu" id="user-menu" tabindex="-1">
% if resume_block:
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${resume_block}" role="menuitem">${_("Resume your last course")}</a></div>
% endif
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('dashboard')}" role="menuitem">${_("Dashboard")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('learner_profile', kwargs={'username': username})}" role="menuitem">${_("Profile")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('account_settings')}" role="menuitem">${_("Account")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('logout')}" role="menuitem">${_("Sign Out")}</a></div>
</div>

View File

@@ -1,11 +1,22 @@
""" Unit tests for custom UserProfile properties. """
from __future__ import absolute_import, division, print_function, unicode_literals
import ddt
from django.test import TestCase
from openedx.core.djangolib.testing.utils import skip_unless_lms
from django.test.utils import override_settings
from mock import patch
from ..utils import validate_social_link, format_social_link
from completion import models
from completion.test_utils import CompletionWaffleTestMixin
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..utils import format_social_link, validate_social_link
@ddt.ddt
@@ -51,3 +62,77 @@ class UserAccountSettingsTest(TestCase):
self.assertEqual(is_valid_expected, self.validate_social_link(platform_name, link_input))
self.assertEqual(formatted_link_expected, format_social_link(platform_name, link_input))
@ddt.ddt
class CompletionUtilsTestCase(SharedModuleStoreTestCase, CompletionWaffleTestMixin, TestCase):
"""
Test completion utility functions
"""
def setUp(self):
"""
Creates a test course that can be used for non-destructive tests
"""
super(CompletionUtilsTestCase, self).setUp()
self.override_waffle_switch(True)
self.engaged_user = UserFactory.create()
self.cruft_user = UserFactory.create()
self.course = self.create_test_course()
self.submit_faux_completions()
def create_test_course(self):
"""
Create, populate test course.
"""
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
self.chapter = ItemFactory.create(category='chapter', parent_location=course.location)
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
self.vertical1 = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
self.vertical2 = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
course.children = [self.chapter]
self.chapter.children = [self.sequential]
self.sequential.children = [self.vertical1, self.vertical2]
if hasattr(self, 'user_one'):
CourseEnrollment.enroll(self.engaged_user, course.id)
if hasattr(self, 'user_two'):
CourseEnrollment.enroll(self.cruft_user, course.id)
return course
def submit_faux_completions(self):
"""
Submit completions (only for user_one)g
"""
for block in self.course.children[0].children[0].children:
models.BlockCompletion.objects.submit_completion(
user=self.engaged_user,
course_key=self.course.id,
block_key=block.location,
completion=1.0
)
@override_settings(LMS_ROOT_URL='test_url:9999')
@patch('completion.waffle.get_current_site')
@ddt.data(True, False)
def test_retrieve_last_sitewide_block_completed(self, use_username, get_patched_current_site): # pylint: disable=unused-argument
"""
Test that the method returns a URL for the "last completed" block
when sending a user object
"""
block_url = retrieve_last_sitewide_block_completed(
self.engaged_user.username if use_username else self.engaged_user
)
empty_block_url = retrieve_last_sitewide_block_completed(
self.cruft_user.username if use_username else self.cruft_user
)
self.assertEqual(
block_url,
u'test_url:9999/courses/{org}/{course}/{run}/jump_to/i4x://{org}/{course}/vertical/{vertical_id}'.format(
org=self.course.location.course_key.org,
course=self.course.location.course_key.course,
run=self.course.location.course_key.run,
vertical_id=self.vertical2.location.block_id,
)
)
self.assertEqual(empty_block_url, None)

View File

@@ -5,7 +5,15 @@ import re
from urlparse import urlparse
from django.conf import settings
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
from six import text_type
from completion import waffle as completion_waffle
from completion.models import BlockCompletion
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.theming.helpers import get_config_value_from_site_or_settings, get_current_site
from xmodule.modulestore.django import modulestore
def validate_social_link(platform_name, new_social_link):
@@ -89,3 +97,77 @@ def _is_valid_social_username(value):
in the username.
"""
return '/' not in value
def retrieve_last_sitewide_block_completed(username):
"""
Completion utility
From a string 'username' or object User retrieve
the last course block marked as 'completed' and construct a URL
:param username: str(username) or obj(User)
:return: block_lms_url
"""
if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING):
return
if not isinstance(username, User):
userobj = User.objects.get(username=username)
else:
userobj = username
latest_completions_by_course = BlockCompletion.latest_blocks_completed_all_courses(userobj)
known_site_configs = [
other_site_config.get_value('course_org_filter') for other_site_config in SiteConfiguration.objects.all()
if other_site_config.get_value('course_org_filter')
]
current_site_configuration = get_config_value_from_site_or_settings(
name='course_org_filter',
site=get_current_site()
)
# courses.edx.org has no 'course_org_filter'
# however the courses within DO, but those entries are not found in
# known_site_configs, which are White Label sites
# This is necessary because the WL sites and courses.edx.org
# have the same AWS RDS mySQL instance
candidate_course = None
candidate_block_key = None
latest_date = None
# Go through dict, find latest
for course, [modified_date, block_key] in latest_completions_by_course.items():
if not current_site_configuration:
# This is a edx.org
if course.org in known_site_configs:
continue
if not latest_date or modified_date > latest_date:
candidate_course = course
candidate_block_key = block_key
latest_date = modified_date
else:
# This is a White Label site, and we should find candidates from the same site
if course.org not in current_site_configuration:
# Not the same White Label, or a edx.org course
continue
if not latest_date or modified_date > latest_date:
candidate_course = course
candidate_block_key = block_key
latest_date = modified_date
if not candidate_course:
return
lms_root = SiteConfiguration.get_value_for_org(candidate_course.org, "LMS_ROOT_URL", settings.LMS_ROOT_URL)
item = modulestore().get_item(candidate_block_key, depth=1)
if not lms_root:
return
return u"{lms_root}/courses/{course_key}/jump_to/{location}".format(
lms_root=lms_root,
course_key=text_type(item.location.course_key),
location=text_type(item.location),
)

View File

@@ -10,6 +10,7 @@ from completion.models import BlockCompletion
from completion.test_utils import CompletionWaffleTestMixin
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import override_settings
from mock import Mock, patch
from six import text_type
@@ -402,6 +403,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
),
active=True
)
@override_settings(LMS_BASE='test_url:9999')
@patch('completion.waffle.get_current_site')
def test_resume_course_with_completion_api(self, get_patched_current_site):
"""