Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 17 additions & 28 deletions lms/djangoapps/courseware/access_response.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
This file contains all the classes used by has_access for error handling
"""

from datetime import datetime

from django.utils.translation import gettext as _

Expand Down Expand Up @@ -124,46 +124,35 @@ 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):
def __init__(
self,
start_date: datetime,
display_error_to_user=True,
error_code_override: str | None = None,
developer_message_override: str | None = None,
user_message_override: str | None = None,
):
"""
Arguments:
start_date: The future start date of the course, used for messaging.
display_error_to_user: If True, display this error to users in the UI.
error_code_override: Optional override error_code.
developer_message_override: Optional override developer_message.
user_message_override: Optional override user_message.
"""
error_code = "course_not_started"
error_code = error_code_override or "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
)

# Use override message if available.
developer_message = developer_message_override or developer_message
user_message = user_message_override or user_message

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,
Expand Down
75 changes: 15 additions & 60 deletions lms/djangoapps/courseware/access_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from crum import get_current_request
from django.conf import settings
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomerUser
from openedx_filters.learning.filters import CourseStartDateValidationFailed
from pytz import UTC

from common.djangoapps.student.models import CourseEnrollment
Expand All @@ -19,7 +20,6 @@
DataSharingConsentRequiredAccessError,
EnrollmentRequiredAccessError,
IncorrectActiveEnterpriseAccessError,
StartDateEnterpriseLearnerError,
StartDateError,
)
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student
Expand Down Expand Up @@ -66,60 +66,6 @@ def adjust_start_date(user, days_early_for_beta, start, course_key):
return start


def enterprise_learner_enrolled(request, user, course_key):
"""
Determine if the learner should be redirected to the enterprise learner portal by checking their enterprise
memberships/enrollments. If all of the following are true, then we are safe to redirect the learner:

* The learner is linked to an enterprise customer,
* The enterprise customer has subsidized the learner's enrollment in the requested course,
* The enterprise customer has the learner portal enabled.

NOTE: This function MUST be called from a view, or it will throw an exception.

Args:
request (django.http.HttpRequest): The current request being handled. Must not be None.
user (User): The requesting enter, potentially an enterprise learner.
course_key (str): The requested course to check for enrollment.

Returns:
bool: True if the learner is enrolled via a linked enterprise customer and can safely be redirected to the
enterprise learner dashboard.
"""
from openedx.features.enterprise_support.api import enterprise_customer_from_session_or_learner_data

if not user.is_authenticated:
return False

# enterprise_customer_data is either None (if learner is not linked to any customer) or a serialized
# EnterpriseCustomer representing the learner's active linked customer.
enterprise_customer_data = enterprise_customer_from_session_or_learner_data(request)
learner_portal_enabled = enterprise_customer_data and enterprise_customer_data["enable_learner_portal"]
if not learner_portal_enabled:
return False

# Additionally make sure the enterprise learner is actually enrolled in the requested course, subsidized
# via the discovered customer.
enterprise_enrollments = EnterpriseCourseEnrollment.objects.filter(
course_id=course_key,
enterprise_customer_user__user_id=user.id,
enterprise_customer_user__enterprise_customer__uuid=enterprise_customer_data["uuid"],
)
enterprise_enrollment_exists = enterprise_enrollments.exists()
log.info(
(
"[enterprise_learner_enrolled] Checking for an enterprise enrollment for "
"lms_user_id=%s in course_key=%s via enterprise_customer_uuid=%s. "
"Exists: %s"
),
user.id,
course_key,
enterprise_customer_data["uuid"],
enterprise_enrollment_exists,
)
return enterprise_enrollment_exists


def check_start_date(user, days_early_for_beta, start, course_key, display_error_to_user=True, now=None):
"""
Verifies whether the given user is allowed access given the
Expand Down Expand Up @@ -148,11 +94,20 @@ def check_start_date(user, days_early_for_beta, start, course_key, display_error
if should_grant_access:
return ACCESS_GRANTED

# Before returning a StartDateError, determine if the learner should be redirected to the enterprise learner
# portal by returning StartDateEnterpriseLearnerError instead.
request = get_current_request()
if request and enterprise_learner_enrolled(request, user, course_key):
return StartDateEnterpriseLearnerError(start, display_error_to_user=display_error_to_user)
# Before returning a StartDateError, give plugins a chance to substitute a more specific access-error payload.
try:
CourseStartDateValidationFailed.run_filter(
course_key=course_key,
start_date=start,
)
except CourseStartDateValidationFailed.OverrideStartDateError as exc:
return StartDateError(
start_date=start,
display_error_to_user=display_error_to_user,
error_code_override=exc.error_code,
developer_message_override=exc.developer_message,
user_message_override=exc.user_message,
)

return StartDateError(start, display_error_to_user=display_error_to_user)
Comment thread
pwnage101 marked this conversation as resolved.

Expand Down
78 changes: 1 addition & 77 deletions lms/djangoapps/courseware/tests/test_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,6 @@
)
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions import MINIMUM_UNUSED_PARTITION_ID, Group, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from enterprise.api.v1.serializers import EnterpriseCustomerSerializer
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory,
EnterpriseCustomerFactory
)
from crum import set_current_request

QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES

Expand Down Expand Up @@ -817,7 +809,7 @@ def test_course_overview_unsupported_action(self):
)
@ddt.unpack
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_course_catalog_access_num_queries_no_enterprise(self, user_attr_name, action, course_attr_name):
def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2018, 1, 1))

course = getattr(self, course_attr_name)
Expand Down Expand Up @@ -856,71 +848,3 @@ def test_course_catalog_access_num_queries_no_enterprise(self, user_attr_name, a
course_overview = CourseOverview.get_from_id(course.id)
with self.assertNumQueries(num_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
bool(access.has_access(user, action, course_overview, course_key=course.id))

@ddt.data(
*itertools.product(
['user_normal', 'user_staff', 'user_anonymous'],
['course_started', 'course_not_started'],
)
)
@ddt.unpack
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False, 'ENABLE_ENTERPRISE_INTEGRATION': True})
def test_course_catalog_access_num_queries_enterprise(self, user_attr_name, course_attr_name):
"""
Similar to test_course_catalog_access_num_queries_no_enterprise, except enable enterprise features and make the
basic enrollment look like an enterprise-subsidized enrollment, setting up one of each:

* EnterpriseCustomer
* EnterpriseCustomerUser
* EnterpriseCourseEnrollment
* A mock request session to pre-cache the enterprise customer data.
"""
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2018, 1, 1))

course = getattr(self, course_attr_name)

request = RequestFactory().get('/')
request.session = {}

# get a fresh user object that won't have any cached role information
if user_attr_name == 'user_anonymous':
user = AnonymousUserFactory()
request.user = user
else:
user = getattr(self, user_attr_name)
user = User.objects.get(id=user.id)
request.user = user
course_enrollment = CourseEnrollmentFactory(user=user, course_id=course.id)
enterprise_customer = EnterpriseCustomerFactory(enable_learner_portal=True)
add_enterprise_customer_to_session(request, EnterpriseCustomerSerializer(enterprise_customer).data)
enterprise_customer_user = EnterpriseCustomerUserFactory(
user_id=user.id,
enterprise_customer=enterprise_customer,
)
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
set_current_request(request)

if user_attr_name == 'user_staff':
if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role
num_queries = 2
else:
# read: CourseAccessRole + EnterpriseCourseEnrollment
num_queries = 2
elif user_attr_name == 'user_normal':
if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role + FBEEnrollmentExclusion + CourseMode
num_queries = 4
else:
# read: CourseAccessRole + CourseEnrollmentAllowed + EnterpriseCourseEnrollment
num_queries = 3
elif user_attr_name == 'user_anonymous':
if course_attr_name == 'course_started':
# read: CourseMode
num_queries = 1
else:
num_queries = 0

course_overview = CourseOverview.get_from_id(course.id)
with self.assertNumQueries(num_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
bool(access.has_access(user, 'see_exists', course_overview, course_key=course.id))
60 changes: 21 additions & 39 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
from freezegun import freeze_time
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_filters.learning.filters import CoursewareViewStarted
from openedx_filters.learning.filters import CourseStartDateValidationFailed, CoursewareViewStarted
from pytz import UTC
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
from rest_framework import status
Expand Down Expand Up @@ -107,13 +107,6 @@
get_learning_mfe_home_url,
make_learning_mfe_courseware_url
)
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory,
EnterpriseCustomerFactory
)
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from enterprise.api.v1.serializers import EnterpriseCustomerSerializer

QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES

Expand Down Expand Up @@ -2645,66 +2638,55 @@ class AccessUtilsTestCase(ModuleStoreTestCase):
@ddt.data(
{
'start_date_modifier': 1, # course starts in future
'setup_enterprise_enrollment': False,
'filter_raises_override': False,
'expected_has_access': False,
'expected_error_code': 'course_not_started',
},
{
'start_date_modifier': -1, # course already started
'setup_enterprise_enrollment': False,
'filter_raises_override': False,
'expected_has_access': True,
'expected_error_code': None,
},
{
'start_date_modifier': 1, # course starts in future
'setup_enterprise_enrollment': True,
'start_date_modifier': 1, # course starts in future, filter overrides error
'filter_raises_override': True,
'expected_has_access': False,
'expected_error_code': 'course_not_started_enterprise_learner',
},
{
'start_date_modifier': -1, # course already started
'setup_enterprise_enrollment': True,
'expected_has_access': True,
'expected_error_code': None,
},
)
@ddt.unpack
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False, 'ENABLE_ENTERPRISE_INTEGRATION': True})
def test_is_course_open_for_learner(
self,
start_date_modifier,
setup_enterprise_enrollment,
filter_raises_override,
expected_has_access,
expected_error_code,
):
"""
Test is_course_open_for_learner().

When setup_enterprise_enrollment == True, make an enterprise-subsidized enrollment, setting up one of each:
* CourseEnrollment
* EnterpriseCustomer
* EnterpriseCustomerUser
* EnterpriseCourseEnrollment
* A mock request session to pre-cache the enterprise customer data.
"""
"""Test is_course_open_for_learner()."""
staff_user = AdminFactory()
start_date = datetime.now(UTC) + timedelta(days=start_date_modifier)
course = CourseFactory.create(start=start_date)
request = RequestFactory().get('/')
request.user = staff_user
request.session = {}
if setup_enterprise_enrollment:
course_enrollment = CourseEnrollmentFactory(mode=CourseMode.VERIFIED, user=staff_user, course_id=course.id)
enterprise_customer = EnterpriseCustomerFactory(enable_learner_portal=True)
add_enterprise_customer_to_session(request, EnterpriseCustomerSerializer(enterprise_customer).data)
enterprise_customer_user = EnterpriseCustomerUserFactory(
user_id=staff_user.id,
enterprise_customer=enterprise_customer,
)
EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user, course_id=course.id)
set_current_request(request)

access_response = check_course_open_for_learner(staff_user, course)
if filter_raises_override:
# Mock the filter to simulate a plugin substituting the start-date error payload.
with patch(
'openedx_filters.learning.filters.CourseStartDateValidationFailed.run_filter'
) as mock_filter:
mock_filter.side_effect = CourseStartDateValidationFailed.OverrideStartDateError(
message='message',
error_code='course_not_started_enterprise_learner',
developer_message='developer message',
user_message='user message',
)
access_response = check_course_open_for_learner(staff_user, course)
else:
access_response = check_course_open_for_learner(staff_user, course)
assert bool(access_response) == expected_has_access
assert access_response.error_code == expected_error_code

Expand Down
2 changes: 1 addition & 1 deletion requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ django-stubs<6
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
edx-enterprise==8.1.0
edx-enterprise==8.1.1

# Date: 2023-07-26
# Our legacy Sass code is incompatible with anything except this ancient libsass version.
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ edx-drf-extensions==10.6.0
# edxval
# enterprise-integrated-channels
# openedx-learning
edx-enterprise==8.1.0
edx-enterprise==8.1.1
# via
# -c requirements/constraints.txt
# -r requirements/edx/kernel.in
Expand Down
Loading
Loading