Skip to content
Snippets Groups Projects
Unverified Commit 96972887 authored by Calen Pennington's avatar Calen Pennington Committed by GitHub
Browse files

Merge pull request #19065 from cpennington/access-control-messaging

Access control messaging
parents e5473f53 5af7fffc
No related merge requests found
......@@ -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">
&nbsp;${_('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
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment