Implement public cohort
This PR is based on #19284 and is part of the series of work related to the proposal #18134. This PR avoids the assignment of anonymous/unenrolled users to any cohort when course is public. Anonymous or unenrolled users will only see content that does not have a content group assigned. The "View Course" link to the course outline is shown on the course about page for a course marked public/public outline. It also makes course handouts available for public courses (not for public_outline). This PR also hides the different warnings and messages asking the user to sign-in and enroll in the course, when the course is marked public. It modifies the default public_view text to include the component display_name when unenrolled access is not available.
This commit is contained in:
@@ -98,7 +98,7 @@ class ContentLibraryTransformer(FilteringTransformerMixin, BlockStructureTransfo
|
||||
# Save back any changes
|
||||
if any(block_keys[changed] for changed in ('invalid', 'overlimit', 'added')):
|
||||
state_dict['selected'] = list(selected)
|
||||
StudentModule.objects.update_or_create(
|
||||
StudentModule.save_state( # pylint: disable=no-value-for-parameter
|
||||
student=usage_info.user,
|
||||
course_id=usage_info.course_key,
|
||||
module_state_key=block_key,
|
||||
|
||||
@@ -18,6 +18,9 @@ def get_student_module_as_dict(user, course_key, block_key):
|
||||
Returns:
|
||||
StudentModule as a (possibly empty) dict.
|
||||
"""
|
||||
if not user.is_authenticated():
|
||||
return {}
|
||||
|
||||
try:
|
||||
student_module = StudentModule.objects.get(
|
||||
student=user,
|
||||
|
||||
@@ -38,6 +38,7 @@ from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.lib.api.view_utils import LazySequence
|
||||
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
|
||||
from path import Path as path
|
||||
from six import text_type
|
||||
from static_replace import replace_static_urls
|
||||
@@ -643,3 +644,13 @@ def get_course_chapter_ids(course_key):
|
||||
log.exception('Failed to retrieve course from modulestore.')
|
||||
return []
|
||||
return [unicode(chapter_key) for chapter_key in chapter_keys if chapter_key.block_type == 'chapter']
|
||||
|
||||
|
||||
def allow_public_access(course, visibilities):
|
||||
"""
|
||||
This checks if the unenrolled access waffle flag for the course is set
|
||||
and the course visibility matches any of the input visibilities.
|
||||
"""
|
||||
unenrolled_access_flag = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course.id)
|
||||
allow_access = unenrolled_access_flag and course.course_visibility in visibilities
|
||||
return allow_access
|
||||
|
||||
@@ -161,6 +161,18 @@ class StudentModule(models.Model):
|
||||
module_states = module_states.filter(student_id=student_id)
|
||||
return module_states
|
||||
|
||||
@classmethod
|
||||
def save_state(cls, student, course_id, module_state_key, defaults):
|
||||
if not student.is_authenticated():
|
||||
return
|
||||
else:
|
||||
cls.objects.update_or_create(
|
||||
student=student,
|
||||
course_id=course_id,
|
||||
module_state_key=module_state_key,
|
||||
defaults=defaults,
|
||||
)
|
||||
|
||||
|
||||
class BaseStudentModuleHistory(models.Model):
|
||||
"""Abstract class containing most fields used by any class
|
||||
|
||||
@@ -3,6 +3,7 @@ Test the about xblock
|
||||
"""
|
||||
import datetime
|
||||
import ddt
|
||||
import mock
|
||||
import pytz
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from django.conf import settings
|
||||
@@ -16,14 +17,22 @@ from waffle.testutils import override_switch
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from openedx.core.lib.tests import attr
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_experience.waffle import WAFFLE_NAMESPACE as COURSE_EXPERIENCE_WAFFLE_NAMESPACE
|
||||
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
|
||||
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
|
||||
from shoppingcart.models import Order, PaidCourseRegistration
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory
|
||||
from track.tests import EventTrackingTestCase
|
||||
from util.milestones_helpers import get_prerequisite_courses_display, set_prerequisite_courses
|
||||
from xmodule.course_module import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
|
||||
from xmodule.course_module import (
|
||||
CATALOG_VISIBILITY_ABOUT,
|
||||
CATALOG_VISIBILITY_NONE,
|
||||
COURSE_VISIBILITY_PRIVATE,
|
||||
COURSE_VISIBILITY_PUBLIC_OUTLINE,
|
||||
COURSE_VISIBILITY_PUBLIC
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_MIXED_MODULESTORE,
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
@@ -41,6 +50,7 @@ REG_STR = "<form id=\"class_enroll_form\" method=\"post\" data-remote=\"true\" a
|
||||
SHIB_ERROR_STR = "The currently logged-in user account does not have permission to enroll in this course."
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@attr(shard=1)
|
||||
class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTrackingTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
@@ -222,6 +232,27 @@ class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTra
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@ddt.data(
|
||||
[COURSE_VISIBILITY_PRIVATE],
|
||||
[COURSE_VISIBILITY_PUBLIC_OUTLINE],
|
||||
[COURSE_VISIBILITY_PUBLIC],
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_about_page_public_view(self, course_visibility):
|
||||
"""
|
||||
Assert that anonymous or unenrolled users see View Course option
|
||||
when unenrolled access flag is set
|
||||
"""
|
||||
with mock.patch('xmodule.course_module.CourseDescriptor.course_visibility', course_visibility):
|
||||
with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True):
|
||||
url = reverse('about_course', args=[text_type(self.course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
if course_visibility == COURSE_VISIBILITY_PUBLIC or course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE:
|
||||
self.assertIn("View Course", resp.content)
|
||||
else:
|
||||
self.assertIn("Enroll in", resp.content)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
|
||||
@@ -2472,6 +2472,10 @@ class TestIndexView(ModuleStoreTestCase):
|
||||
self.assertIn('xblock-public_view-vertical', response.content)
|
||||
self.assertIn('xblock-public_view-html', response.content)
|
||||
self.assertIn('xblock-public_view-video', response.content)
|
||||
if user_type == CourseUserType.ANONYMOUS and course_visibility == COURSE_VISIBILITY_PRIVATE:
|
||||
self.assertIn('To see course content', response.content)
|
||||
if user_type == CourseUserType.UNENROLLED and course_visibility == COURSE_VISIBILITY_PRIVATE:
|
||||
self.assertIn('You must be enrolled', response.content)
|
||||
else:
|
||||
self.assertIn('data-save-position="true"', response.content)
|
||||
self.assertIn('data-show-completion="true"', response.content)
|
||||
|
||||
@@ -25,6 +25,7 @@ from web_fragments.fragment import Fragment
|
||||
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from lms.djangoapps.courseware.courses import allow_public_access
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
|
||||
from lms.djangoapps.gating.api import get_entrance_exam_score_ratio, get_entrance_exam_usage_key
|
||||
@@ -195,20 +196,23 @@ class CoursewareIndex(View):
|
||||
'email_opt_in': False,
|
||||
})
|
||||
|
||||
PageLevelMessages.register_warning_message(
|
||||
request,
|
||||
Text(_(u"You are not signed in. To see additional course content, {sign_in_link} or "
|
||||
u"{register_link}, and enroll in this course.")).format(
|
||||
sign_in_link=HTML(u'<a href="{url}">{sign_in_label}</a>').format(
|
||||
sign_in_label=_('sign in'),
|
||||
url='{}?{}'.format(reverse('signin_user'), qs),
|
||||
),
|
||||
register_link=HTML(u'<a href="/{url}">{register_label}</a>').format(
|
||||
register_label=_('register'),
|
||||
url='{}?{}'.format(reverse('register_user'), qs),
|
||||
),
|
||||
allow_anonymous = allow_public_access(self.course, [COURSE_VISIBILITY_PUBLIC])
|
||||
|
||||
if not allow_anonymous:
|
||||
PageLevelMessages.register_warning_message(
|
||||
request,
|
||||
Text(_("You are not signed in. To see additional course content, {sign_in_link} or "
|
||||
"{register_link}, and enroll in this course.")).format(
|
||||
sign_in_link=HTML('<a href="{url}">{sign_in_label}</a>').format(
|
||||
sign_in_label=_('sign in'),
|
||||
url='{}?{}'.format(reverse('signin_user'), qs),
|
||||
),
|
||||
register_link=HTML('<a href="/{url}">{register_label}</a>').format(
|
||||
register_label=_('register'),
|
||||
url='{}?{}'.format(reverse('register_user'), qs),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return render_to_response('courseware/courseware.html', self._create_courseware_context(request))
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
|
||||
from lms.djangoapps.certificates import api as certs_api
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.courseware.courses import allow_public_access
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
|
||||
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
@@ -87,7 +88,11 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_fragment
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name
|
||||
from openedx.features.course_experience import (
|
||||
UNIFIED_COURSE_TAB_FLAG,
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
course_home_url_name,
|
||||
)
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
|
||||
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
|
||||
@@ -101,6 +106,7 @@ from util.cache import cache, cache_if_anonymous
|
||||
from util.db import outer_atomic
|
||||
from util.milestones_helpers import get_prerequisite_courses_display
|
||||
from util.views import _record_feedback_in_zendesk, ensure_valid_course_key, ensure_valid_usage_key
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE
|
||||
from web_fragments.fragment import Fragment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
@@ -459,7 +465,7 @@ class StaticCourseTabView(EdxFragmentView):
|
||||
raise Http404
|
||||
|
||||
# Show warnings if the user has limited access
|
||||
CourseTabView.register_user_access_warning_messages(request, course_key)
|
||||
CourseTabView.register_user_access_warning_messages(request, course)
|
||||
|
||||
return super(StaticCourseTabView, self).get(request, course=course, tab=tab, **kwargs)
|
||||
|
||||
@@ -504,7 +510,7 @@ class CourseTabView(EdxFragmentView):
|
||||
|
||||
# Show warnings if the user has limited access
|
||||
# Must come after masquerading on creation of page context
|
||||
self.register_user_access_warning_messages(request, course_key)
|
||||
self.register_user_access_warning_messages(request, course)
|
||||
|
||||
set_custom_metrics_for_course_key(course_key)
|
||||
return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs)
|
||||
@@ -522,11 +528,13 @@ class CourseTabView(EdxFragmentView):
|
||||
return url_to_enroll
|
||||
|
||||
@staticmethod
|
||||
def register_user_access_warning_messages(request, course_key):
|
||||
def register_user_access_warning_messages(request, course):
|
||||
"""
|
||||
Register messages to be shown to the user if they have limited access.
|
||||
"""
|
||||
if request.user.is_anonymous:
|
||||
allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC])
|
||||
|
||||
if request.user.is_anonymous and not allow_anonymous:
|
||||
PageLevelMessages.register_warning_message(
|
||||
request,
|
||||
Text(_(u"To see course content, {sign_in_link} or {register_link}.")).format(
|
||||
@@ -541,9 +549,9 @@ class CourseTabView(EdxFragmentView):
|
||||
)
|
||||
)
|
||||
else:
|
||||
if not CourseEnrollment.is_enrolled(request.user, course_key):
|
||||
if not CourseEnrollment.is_enrolled(request.user, course.id) and not allow_anonymous:
|
||||
# Only show enroll button if course is open for enrollment.
|
||||
if course_open_for_self_enrollment(course_key):
|
||||
if course_open_for_self_enrollment(course.id):
|
||||
enroll_message = _(u'You must be enrolled in the course to see course content. \
|
||||
{enroll_link_start}Enroll now{enroll_link_end}.')
|
||||
PageLevelMessages.register_warning_message(
|
||||
@@ -840,6 +848,8 @@ def course_about(request, course_id):
|
||||
|
||||
sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML)
|
||||
|
||||
allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE])
|
||||
|
||||
# This local import is due to the circularity of lms and openedx references.
|
||||
# This may be resolved by using stevedore to allow web fragments to be used
|
||||
# as plugins, and to avoid the direct import.
|
||||
@@ -878,6 +888,7 @@ def course_about(request, course_id):
|
||||
'course_image_urls': overview.image_urls,
|
||||
'reviews_fragment_view': reviews_fragment_view,
|
||||
'sidebar_html_enabled': sidebar_html_enabled,
|
||||
'allow_anonymous': allow_anonymous,
|
||||
}
|
||||
|
||||
return render_to_response('courseware/course_about.html', context)
|
||||
|
||||
@@ -168,6 +168,12 @@ from six import string_types
|
||||
.format(course_name=course.display_number_with_default, price=course_price)}
|
||||
</a>
|
||||
<div id="register_error"></div>
|
||||
%elif allow_anonymous:
|
||||
%if show_courseware_link:
|
||||
<a href="${course_target}">
|
||||
<strong>${_("View Course")}</strong>
|
||||
</a>
|
||||
%endif
|
||||
%else:
|
||||
<%
|
||||
if ecommerce_checkout:
|
||||
|
||||
Reference in New Issue
Block a user