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:
Pooja Kulkarni
2019-02-07 12:33:59 +05:30
parent c0889e16ab
commit 9ddb1cc074
15 changed files with 170 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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