Add "last completed block" to dropdown
This commit is contained in:
committed by
Sanford Student
parent
335bf4b035
commit
a5d8cbb8e9
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user