Merge pull request #19065 from cpennington/access-control-messaging
Access control messaging
This commit is contained in:
@@ -21,6 +21,7 @@ from bulk_email.models import BulkEmailFlag
|
||||
from course_modes.models import CourseMode
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
@@ -289,10 +290,11 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
program = ProgramFactory()
|
||||
CourseEntitlementFactory.create(user=self.user, course_uuid=program['courses'][0]['uuid'])
|
||||
mock_get_programs.return_value = [program]
|
||||
mock_course_overview.return_value = CourseOverviewFactory.create(start=self.TOMORROW)
|
||||
course_key = CourseKey.from_string('course-v1:FAKE+FA1-MA1.X+3T2017')
|
||||
mock_course_overview.return_value = CourseOverviewFactory.create(start=self.TOMORROW, id=course_key)
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
|
||||
'key': unicode(course_key),
|
||||
'enrollment_end': str(self.TOMORROW),
|
||||
'pacing_type': 'instructor_paced',
|
||||
'type': 'verified',
|
||||
@@ -300,7 +302,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
}
|
||||
]
|
||||
mock_pseudo_session.return_value = {
|
||||
'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
|
||||
'key': unicode(course_key),
|
||||
'type': 'verified'
|
||||
}
|
||||
response = self.client.get(self.path)
|
||||
@@ -361,8 +363,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
|
||||
@patch('entitlements.api.v1.views.get_course_runs_for_course')
|
||||
@patch.object(CourseOverview, 'get_from_id')
|
||||
@patch('opaque_keys.edx.keys.CourseKey.from_string')
|
||||
def test_sessions_for_entitlement_course_runs(self, mock_course_key, mock_course_overview, mock_course_runs):
|
||||
def test_sessions_for_entitlement_course_runs(self, mock_course_overview, mock_course_runs):
|
||||
"""
|
||||
When a learner has a fulfilled entitlement for a course run in the past, there should be no availableSession
|
||||
data passed to the JS view. When a learner has a fulfilled entitlement for a course run enrollment ending in the
|
||||
@@ -378,7 +379,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
start=self.TOMORROW, end=self.THREE_YEARS_FROM_NOW, self_paced=True, enrollment_end=self.THREE_YEARS_AGO
|
||||
)
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id))
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
@@ -398,7 +398,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
mocked_course_overview.save()
|
||||
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
'key': str(mocked_course_overview.id),
|
||||
@@ -416,7 +415,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
mocked_course_overview.save()
|
||||
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
'key': str(mocked_course_overview.id),
|
||||
@@ -432,8 +430,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
@patch('openedx.core.djangoapps.programs.utils.get_programs')
|
||||
@patch('student.views.dashboard.get_visible_sessions_for_entitlement')
|
||||
@patch.object(CourseOverview, 'get_from_id')
|
||||
@patch('opaque_keys.edx.keys.CourseKey.from_string')
|
||||
def test_fulfilled_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
|
||||
def test_fulfilled_entitlement(self, mock_course_overview, mock_course_runs, mock_get_programs):
|
||||
"""
|
||||
When a learner has a fulfilled entitlement, their course dashboard should have:
|
||||
- exactly one course item, meaning it:
|
||||
@@ -446,7 +443,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
|
||||
)
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id))
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
@@ -470,8 +466,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
@patch('openedx.core.djangoapps.programs.utils.get_programs')
|
||||
@patch('student.views.dashboard.get_visible_sessions_for_entitlement')
|
||||
@patch.object(CourseOverview, 'get_from_id')
|
||||
@patch('opaque_keys.edx.keys.CourseKey.from_string')
|
||||
def test_fulfilled_expired_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
|
||||
def test_fulfilled_expired_entitlement(self, mock_course_overview, mock_course_runs, mock_get_programs):
|
||||
"""
|
||||
When a learner has a fulfilled entitlement that is expired, their course dashboard should have:
|
||||
- exactly one course item, meaning it:
|
||||
@@ -483,7 +478,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
|
||||
)
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id), created=self.THREE_YEARS_AGO)
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
|
||||
@@ -647,10 +647,10 @@ def student_dashboard(request):
|
||||
staff_access = True
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
show_courseware_links_for = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if has_access(request.user, 'load', enrollment.course_overview)
|
||||
)
|
||||
show_courseware_links_for = {
|
||||
enrollment.course_id: has_access(request.user, 'load', enrollment.course_overview)
|
||||
for enrollment in course_enrollments
|
||||
}
|
||||
|
||||
# Find programs associated with course runs being displayed. This information
|
||||
# is passed in the template context to allow rendering of program-related
|
||||
|
||||
@@ -239,3 +239,33 @@ class UserPartition(namedtuple("UserPartition", "id name description groups sche
|
||||
group_id=group_id, partition_id=self.id
|
||||
)
|
||||
)
|
||||
|
||||
def access_denied_message(self, block, user, user_group, allowed_groups):
|
||||
"""
|
||||
Return a message that should be displayed to the user when they are not allowed to access
|
||||
content managed by this partition, or None if there is no applicable message.
|
||||
|
||||
Arguments:
|
||||
block (:class:`.XBlock`): The content being managed
|
||||
user (:class:`.User`): The user who was denied access
|
||||
user_group (:class:`.Group`): The current Group the user is in
|
||||
allowed_groups (list of :class:`.Group`): The groups who are allowed to see the content
|
||||
|
||||
Returns: str
|
||||
"""
|
||||
return None
|
||||
|
||||
def access_denied_fragment(self, block, user, course_key, user_group, allowed_groups):
|
||||
"""
|
||||
Return an html fragment that should be displayed to the user when they are not allowed to access
|
||||
content managed by this partition, or None if there is no applicable message.
|
||||
|
||||
Arguments:
|
||||
block (:class:`.XBlock`): The content being managed
|
||||
user (:class:`.User`): The user who was denied access
|
||||
user_group (:class:`.Group`): The current Group the user is in
|
||||
allowed_groups (list of :class:`.Group`): The groups who are allowed to see the content
|
||||
|
||||
Returns: :class:`.Fragment`
|
||||
"""
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,7 @@ from xmodule.course_metadata_utils import DEFAULT_START_DATE
|
||||
|
||||
class AccessResponse(object):
|
||||
"""Class that represents a response from a has_access permission check."""
|
||||
def __init__(self, has_access, error_code=None, developer_message=None, user_message=None):
|
||||
def __init__(self, has_access, error_code=None, developer_message=None, user_message=None, user_fragment=None):
|
||||
"""
|
||||
Creates an AccessResponse object.
|
||||
|
||||
@@ -21,11 +21,14 @@ class AccessResponse(object):
|
||||
to show the developer
|
||||
user_message (String): optional - default is None. Message to
|
||||
show the user
|
||||
user_fragment (:py:class:`~web_fragments.fragment.Fragment`): optional -
|
||||
An html fragment to display to the user if their access is denied.
|
||||
"""
|
||||
self.has_access = has_access
|
||||
self.error_code = error_code
|
||||
self.developer_message = developer_message
|
||||
self.user_message = user_message
|
||||
self.user_fragment = user_fragment
|
||||
if has_access:
|
||||
assert error_code is None
|
||||
|
||||
@@ -54,15 +57,29 @@ class AccessResponse(object):
|
||||
"has_access": self.has_access,
|
||||
"error_code": self.error_code,
|
||||
"developer_message": self.developer_message,
|
||||
"user_message": self.user_message
|
||||
"user_message": self.user_message,
|
||||
"user_fragment": self.user_fragment,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return "AccessResponse({!r}, {!r}, {!r}, {!r})".format(
|
||||
return "AccessResponse({!r}, {!r}, {!r}, {!r}, {!r})".format(
|
||||
self.has_access,
|
||||
self.error_code,
|
||||
self.developer_message,
|
||||
self.user_message
|
||||
self.user_message,
|
||||
self.user_fragment,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, AccessResponse):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.has_access == other.has_access and
|
||||
self.error_code == other.error_code and
|
||||
self.developer_message == other.developer_message and
|
||||
self.user_message == other.user_message and
|
||||
self.user_fragment == other.user_fragment
|
||||
)
|
||||
|
||||
|
||||
@@ -72,7 +89,7 @@ class AccessError(AccessResponse):
|
||||
denial in has_access. Contains the error code, user and developer
|
||||
messages. Subclasses represent specific errors.
|
||||
"""
|
||||
def __init__(self, error_code, developer_message, user_message):
|
||||
def __init__(self, error_code, developer_message, user_message, user_fragment=None):
|
||||
"""
|
||||
Creates an AccessError object.
|
||||
|
||||
@@ -83,9 +100,10 @@ class AccessError(AccessResponse):
|
||||
error_code (String): unique identifier for the specific type of
|
||||
error developer_message (String): message to show the developer
|
||||
user_message (String): message to show the user
|
||||
user_fragment (:py:class:`~web_fragments.fragment.Fragment`): HTML to show the user
|
||||
|
||||
"""
|
||||
super(AccessError, self).__init__(False, error_code, developer_message, user_message)
|
||||
super(AccessError, self).__init__(False, error_code, developer_message, user_message, user_fragment)
|
||||
|
||||
|
||||
class StartDateError(AccessError):
|
||||
|
||||
@@ -1342,7 +1342,7 @@ p.course-block {
|
||||
|
||||
.enter-course-blocked {
|
||||
@include box-sizing(border-box);
|
||||
@include float(left);
|
||||
@include float(right);
|
||||
|
||||
display: block;
|
||||
font: normal 15px/1.6rem $font-family-sans-serif;
|
||||
|
||||
@@ -175,7 +175,7 @@ from student.models import CourseEnrollment
|
||||
show_email_settings = (enrollment.course_id in show_email_settings_for)
|
||||
|
||||
session_id = enrollment.course_id
|
||||
show_courseware_link = (session_id in show_courseware_links_for)
|
||||
show_courseware_link = show_courseware_links_for.get(session_id, False)
|
||||
cert_status = cert_statuses.get(session_id)
|
||||
can_refund_entitlement = entitlement and entitlement.is_entitlement_refundable()
|
||||
can_unenroll = (not cert_status) or cert_status.get('can_unenroll') if not unfulfilled_entitlement else False
|
||||
|
||||
@@ -184,8 +184,8 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
</span>
|
||||
</a>
|
||||
% elif not is_course_blocked:
|
||||
<a href="${course_target}"
|
||||
class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}"
|
||||
<a href="${course_target}"
|
||||
class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}"
|
||||
data-course-key="${enrollment.course_id}">
|
||||
${_('View Course')}
|
||||
<span class="sr">
|
||||
@@ -202,6 +202,14 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
</a>
|
||||
% endif
|
||||
% endif
|
||||
% elif hasattr(show_courseware_link, 'user_message'):
|
||||
<span class="enter-course-blocked"
|
||||
data-course-key="${enrollment.course_id}">
|
||||
${show_courseware_link.user_message}
|
||||
<span class="sr">
|
||||
${_('for {course_display_name}').format(course_display_name=course_overview.display_name_with_default)}
|
||||
</span>
|
||||
</span>
|
||||
% endif
|
||||
|
||||
% if show_courseware_link or course_overview.has_social_sharing_url() or course_overview.has_marketing_url():
|
||||
|
||||
@@ -190,7 +190,7 @@ from student.models import CourseEnrollment
|
||||
show_email_settings = (enrollment.course_id in show_email_settings_for)
|
||||
|
||||
session_id = enrollment.course_id
|
||||
show_courseware_link = (session_id in show_courseware_links_for)
|
||||
show_courseware_link = show_courseware_links_for.get(session_id, False)
|
||||
cert_status = cert_statuses.get(session_id)
|
||||
can_refund_entitlement = entitlement and entitlement.is_entitlement_refundable()
|
||||
can_unenroll = (not cert_status) or cert_status.get('can_unenroll') if not unfulfilled_entitlement else False
|
||||
|
||||
Reference in New Issue
Block a user