Merge pull request #19095 from edx/expired_dashboard_message
Add course duration limit and dashboard expiration code
This commit is contained in:
0
openedx/features/course_duration_limits/__init__.py
Normal file
0
openedx/features/course_duration_limits/__init__.py
Normal file
75
openedx/features/course_duration_limits/access.py
Normal file
75
openedx/features/course_duration_limits/access.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Contains code related to computing content gating course duration limits
|
||||
and course access based on these limits.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.apps import apps
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized
|
||||
from lms.djangoapps.courseware.access_response import AccessError
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
class AuditExpiredError(AccessError):
|
||||
"""
|
||||
Access denied because the user's audit timespan has expired
|
||||
"""
|
||||
def __init__(self, user, course, expiration_date):
|
||||
error_code = "audit_expired"
|
||||
developer_message = "User {} had access to {} until {}".format(user, course, expiration_date)
|
||||
expiration_date = strftime_localized(expiration_date, DEFAULT_SHORT_DATE_FORMAT)
|
||||
user_message = _("Access expired on {expiration_date}").format(expiration_date=expiration_date)
|
||||
try:
|
||||
course_name = CourseOverview.get_from_id(course.id).display_name_with_default
|
||||
additional_context_user_message = _("Access to {course_name} expired on {expiration_date}").format(
|
||||
course_name=course_name,
|
||||
expiration_date=expiration_date
|
||||
)
|
||||
except CourseOverview.DoesNotExist:
|
||||
additional_context_user_message = _("Access to the course you were looking"
|
||||
"for expired on {expiration_date}").format(
|
||||
expiration_date=expiration_date
|
||||
)
|
||||
super(AuditExpiredError, self).__init__(error_code, developer_message, user_message,
|
||||
additional_context_user_message)
|
||||
|
||||
|
||||
def get_user_course_expiration_date(user, course):
|
||||
"""
|
||||
Return course expiration date for given user course pair.
|
||||
Return None if the course does not expire.
|
||||
"""
|
||||
# TODO: Update business logic based on REV-531
|
||||
CourseEnrollment = apps.get_model('student.CourseEnrollment')
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course.id)
|
||||
if enrollment is None or enrollment.mode == 'verified':
|
||||
return None
|
||||
|
||||
try:
|
||||
start_date = enrollment.schedule.start
|
||||
except CourseEnrollment.schedule.RelatedObjectDoesNotExist:
|
||||
start_date = max(enrollment.created, course.start)
|
||||
|
||||
access_duration = timedelta(weeks=8)
|
||||
if hasattr(course, 'pacing') and course.pacing == 'instructor':
|
||||
if course.end and course.start:
|
||||
access_duration = course.end - course.start
|
||||
|
||||
expiration_date = start_date + access_duration
|
||||
return expiration_date
|
||||
|
||||
|
||||
def check_course_expired(user, course):
|
||||
"""
|
||||
Check if the course expired for the user.
|
||||
"""
|
||||
expiration_date = get_user_course_expiration_date(user, course)
|
||||
if expiration_date and timezone.now() > expiration_date:
|
||||
return AuditExpiredError(user, course, expiration_date)
|
||||
|
||||
return ACCESS_GRANTED
|
||||
11
openedx/features/course_duration_limits/apps.py
Normal file
11
openedx/features/course_duration_limits/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Course duration limits application configuration
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CourseDurationLimitsConfig(AppConfig):
|
||||
name = 'openedx.features.course_duration_limits'
|
||||
13
openedx/features/course_duration_limits/config.py
Normal file
13
openedx/features/course_duration_limits/config.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Content type gating waffle flag
|
||||
"""
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleFlag
|
||||
|
||||
|
||||
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=u'content_type_gating')
|
||||
|
||||
CONTENT_TYPE_GATING_FLAG = WaffleFlag(
|
||||
waffle_namespace=WAFFLE_FLAG_NAMESPACE,
|
||||
flag_name=u'debug',
|
||||
flag_undefined_default=False
|
||||
)
|
||||
@@ -20,7 +20,10 @@ from courseware.tests.factories import StaffFactory
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
|
||||
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
|
||||
from openedx.features.course_experience import (
|
||||
SHOW_REVIEWS_TOOL_FLAG,
|
||||
SHOW_UPGRADE_MSG_ON_COURSE_HOME,
|
||||
@@ -165,6 +168,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200)
|
||||
|
||||
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
|
||||
def test_queries(self):
|
||||
"""
|
||||
Verify that the view's query count doesn't regress.
|
||||
@@ -173,7 +177,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
course_home_url(self.course)
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
with self.assertNumQueries(54, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(66, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
@@ -317,6 +321,35 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
|
||||
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
|
||||
def test_expired_course(self):
|
||||
"""
|
||||
Ensure that a user accessing an expired course sees a redirect to
|
||||
the student dashboard, not a 404.
|
||||
"""
|
||||
three_years_ago = now() - timedelta(days=(365 * 3))
|
||||
course = CourseFactory.create(start=three_years_ago)
|
||||
user = self.create_user_for_course(course, CourseUserType.ENROLLED)
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course.id)
|
||||
ScheduleFactory(start=three_years_ago, enrollment=enrollment)
|
||||
|
||||
url = course_home_url(course)
|
||||
response = self.client.get(url)
|
||||
|
||||
expiration_date = strftime_localized(course.start + timedelta(weeks=8), 'SHORT_DATE')
|
||||
expected_params = QueryDict(mutable=True)
|
||||
course_name = CourseOverview.get_from_id(course.id).display_name_with_default
|
||||
expected_params['access_response_error'] = 'Access to {run} expired on {expiration_date}'.format(
|
||||
run=course_name,
|
||||
expiration_date=expiration_date
|
||||
)
|
||||
expected_url = '{url}?{params}'.format(
|
||||
url=reverse('dashboard'),
|
||||
params=expected_params.urlencode()
|
||||
)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
|
||||
@mock.patch("util.date_utils.strftime_localized")
|
||||
def test_non_live_course_other_language(self, mock_strftime_localized):
|
||||
|
||||
@@ -3,7 +3,8 @@ Tests for the course updates page.
|
||||
"""
|
||||
from courseware.courses import get_course_info_usage_key
|
||||
from django.urls import reverse
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
|
||||
from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
|
||||
from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -117,6 +118,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
self.assertContains(response, 'First Message')
|
||||
self.assertContains(response, 'Second Message')
|
||||
|
||||
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
|
||||
def test_queries(self):
|
||||
create_course_update(self.course, self.user, 'First Message')
|
||||
|
||||
@@ -124,7 +126,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
course_updates_url(self.course)
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
with self.assertNumQueries(34, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(38, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
Reference in New Issue
Block a user