Files
edx-platform/lms/djangoapps/courseware/access_response.py
Troy Sankey 499679ab6c feat: add new course access error_code for enterprise learners in future courses (2nd try)
Normally, the course API would return an access error_code of
`course_not_started` if the course has not started yet.  This change
breaks that up into two codes:

* if the course has not started:
  * return error_code=`course_not_started_enterprise_learner` if the
    learner is enrolled as a subsidized enterprise learner.
  * else, return error_code=`course_not_started`.

This supports a change to the frontend which will interpret
`course_not_started_enterprise_learner` differently and trigger a
redirect to the enterprise (B2B) learner dashboard instead of the B2C
dashboard.

ENT-8078
2023-12-18 12:55:17 -08:00

306 lines
12 KiB
Python

"""
This file contains all the classes used by has_access for error handling
"""
from django.utils.translation import gettext as _
from xmodule.course_metadata_utils import DEFAULT_START_DATE
class AccessResponse:
"""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,
additional_context_user_message=None, user_fragment=None):
"""
Creates an AccessResponse object.
Arguments:
has_access (bool): if the user is granted access or not
error_code (String): optional - default is None. Unique identifier
for the specific type of error
developer_message (String): optional - default is None. Message
to show the developer
user_message (String): optional - default is None. Message to
show the user
additional_context_user_message (String): optional - default is None. Message to
show the user when additional context like the course name is necessary
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.additional_context_user_message = additional_context_user_message
self.user_fragment = user_fragment
if has_access:
assert error_code is None
def __bool__(self):
"""
Overrides bool().
Allows for truth value testing of AccessResponse objects, so callers
who do not need the specific error information can check if access
is granted.
Returns:
bool: whether or not access is granted
"""
return self.has_access
__nonzero__ = __bool__
def to_json(self):
"""
Creates a serializable JSON representation of an AccessResponse object.
Returns:
dict: JSON representation
"""
return {
"has_access": self.has_access,
"error_code": self.error_code,
"developer_message": self.developer_message,
"user_message": self.user_message,
"additional_context_user_message": self.additional_context_user_message,
"user_fragment": self.user_fragment,
}
def __repr__(self):
return "AccessResponse({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
self.has_access,
self.error_code,
self.developer_message,
self.user_message,
self.additional_context_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.additional_context_user_message == other.additional_context_user_message and
self.user_fragment == other.user_fragment
)
class AccessError(AccessResponse):
"""
Class that holds information about the error in the case of an access
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,
additional_context_user_message=None, user_fragment=None):
"""
Creates an AccessError object.
An AccessError object represents an AccessResponse where access is
denied (has_access is False).
Arguments:
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
additional_context_user_message (String): message to show user with additional context like the course name
user_fragment (:py:class:`~web_fragments.fragment.Fragment`): HTML to show the user
"""
super().__init__(False, error_code, developer_message,
user_message, additional_context_user_message, user_fragment)
class StartDateError(AccessError):
"""
Access denied because the course has not started yet and the user
is not staff
"""
def __init__(self, start_date, display_error_to_user=True):
"""
Arguments:
display_error_to_user: If True, display this error to users in the UI.
"""
error_code = "course_not_started"
if start_date == DEFAULT_START_DATE:
developer_message = "Course has not started"
user_message = _("Course has not started")
else:
developer_message = f"Course does not start until {start_date}"
user_message = _("Course does not start until {}" # lint-amnesty, pylint: disable=translation-of-non-string
.format(f"{start_date:%B %d, %Y}"))
super().__init__(
error_code,
developer_message,
user_message if display_error_to_user else None
)
class StartDateEnterpriseLearnerError(AccessError):
"""
Access denied because the course has not started yet and the user is not staff. Use this error when this user is
also an enterprise learner and enrolled in the requested course.
"""
def __init__(self, start_date, display_error_to_user=True):
"""
Arguments:
display_error_to_user: If True, display this error to users in the UI.
"""
error_code = "course_not_started_enterprise_learner"
if start_date == DEFAULT_START_DATE:
developer_message = "Course has not started, and the learner is enrolled via an enterprise subsidy."
user_message = _("Course has not started")
else:
developer_message = (
f"Course does not start until {start_date}, and the learner is enrolled via an enterprise subsidy."
)
user_message = _("Course does not start until {}" # lint-amnesty, pylint: disable=translation-of-non-string
.format(f"{start_date:%B %d, %Y}"))
super().__init__(
error_code,
developer_message,
user_message if display_error_to_user else None
)
class MilestoneAccessError(AccessError):
"""
Access denied because the user has unfulfilled milestones
"""
def __init__(self):
error_code = "unfulfilled_milestones"
developer_message = "User has unfulfilled milestones"
user_message = _("You have unfulfilled milestones")
super().__init__(error_code, developer_message, user_message)
class VisibilityError(AccessError):
"""
Access denied because the user does have the correct role to view this
course.
"""
def __init__(self, display_error_to_user=True):
"""
Arguments:
display_error_to_user: Should a message showing that access was denied to this content
be shown to the user?
"""
error_code = "not_visible_to_user"
developer_message = "Course is not visible to this user"
user_message = _("You do not have access to this course")
super().__init__(
error_code,
developer_message,
user_message if display_error_to_user else None
)
class MobileAvailabilityError(AccessError):
"""
Access denied because the course is not available on mobile for the user
"""
def __init__(self):
error_code = "mobile_unavailable"
developer_message = "Course is not available on mobile for this user"
user_message = _("You do not have access to this course on a mobile device")
super().__init__(error_code, developer_message, user_message)
class IncorrectPartitionGroupError(AccessError):
"""
Access denied because the user is not in the correct user subset.
"""
def __init__(self, partition, user_group, allowed_groups, user_message=None, user_fragment=None):
error_code = "incorrect_user_group"
developer_message = "In partition {}, user was in group {}, but only {} are allowed access".format(
partition.name,
user_group.name if user_group is not None else user_group,
", ".join(group.name for group in allowed_groups),
)
super().__init__(
error_code=error_code,
developer_message=developer_message,
user_message=user_message,
user_fragment=user_fragment
)
class NoAllowedPartitionGroupsError(AccessError):
"""
Access denied because the content is not allowed to any group in a partition.
"""
def __init__(self, partition, user_message=None, user_fragment=None):
error_code = "no_allowed_user_groups"
developer_message = f"Group access for {partition.name} excludes all students"
super().__init__(error_code, developer_message, user_message)
class EnrollmentRequiredAccessError(AccessError):
"""
Access denied because the user must be enrolled in the course
"""
def __init__(self):
error_code = "enrollment_required"
developer_message = "User must be enrolled in the course"
user_message = _("You must be enrolled in the course")
super().__init__(error_code, developer_message, user_message)
class IncorrectActiveEnterpriseAccessError(AccessError):
"""
Access denied because the user must login with correct enterprise.
"""
def __init__(self, enrollment_enterprise_name, active_enterprise_name):
error_code = "incorrect_active_enterprise"
developer_message = "User active enterprise should be same as EnterpriseCourseEnrollment enterprise."
user_message = _("You are enrolled in this course with '{enrollment_enterprise_name}'. However, you are "
"currently logged in as a '{active_enterprise_name}' user. Please log in with "
"'{enrollment_enterprise_name}' to access this course.")
user_message = user_message.format(
enrollment_enterprise_name=enrollment_enterprise_name, active_enterprise_name=active_enterprise_name
)
super().__init__(error_code, developer_message, user_message)
class DataSharingConsentRequiredAccessError(AccessError):
"""
Access denied because the user must give Data sharing consent before access it.
"""
def __init__(self, consent_url):
error_code = "data_sharing_access_required"
developer_message = consent_url
user_message = _("You must give Data Sharing Consent for the course")
super().__init__(error_code, developer_message, user_message)
class AuthenticationRequiredAccessError(AccessError):
"""
Access denied because the user must be authenticated to see it
"""
def __init__(self):
error_code = "authentication_required"
developer_message = "User must be authenticated to view the course"
user_message = _("You must be logged in to see this course")
super().__init__(error_code, developer_message, user_message)
class OldMongoAccessError(AccessError):
"""
Access denied because the course is in Old Mongo and we no longer support them. See DEPR-58.
"""
def __init__(self, courselike):
error_code = 'old_mongo'
developer_message = 'Access to Old Mongo courses is unsupported'
user_message = _('{course_name} is no longer available.').format(
course_name=courselike.display_name_with_default,
)
super().__init__(error_code, developer_message, user_message)