# -*- coding: utf-8 -*-
"""
Contains code related to computing content gating course duration limits
and course access based on these limits.
"""
import crum
from django.utils import timezone
from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from edx_django_utils.cache import RequestCache
from web_fragments.fragment import Fragment
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.courseware.access_response import AccessError
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_specific_student
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_date_signals.utils import get_expected_duration
from openedx.core.djangolib.markup import HTML
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
EXPIRATION_DATE_FORMAT_STR = u'%b %-d, %Y'
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 = u"User {} had access to {} until {}".format(user, course, expiration_date)
expiration_date = strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
user_message = _(u"Access expired on {expiration_date}").format(expiration_date=expiration_date)
try:
course_name = course.display_name_with_default
additional_context_user_message = _(u"Access to {course_name} expired on {expiration_date}").format(
course_name=course_name,
expiration_date=expiration_date
)
except CourseOverview.DoesNotExist:
additional_context_user_message = _(u"Access to the course you were looking"
u" 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_duration(user, course):
"""
Return a timedelta measuring the duration of the course for a particular user.
Business Logic:
- Course access duration is bounded by the min and max duration.
- If course fields are missing, default course access duration to MIN_DURATION.
"""
if not CourseDurationLimitConfig.enabled_for_enrollment(user, course):
return None
enrollment = CourseEnrollment.get_enrollment(user, course.id)
if enrollment is None or enrollment.mode != CourseMode.AUDIT:
return None
verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True)
if not verified_mode:
return None
return get_expected_duration(course.id)
def get_user_course_expiration_date(user, course):
"""
Return expiration date for given user course pair.
Return None if the course does not expire.
Business Logic:
- Course access duration is bounded by the min and max duration.
- If course fields are missing, default course access duration to MIN_DURATION.
"""
access_duration = get_user_course_duration(user, course)
if access_duration is None:
return None
enrollment = CourseEnrollment.get_enrollment(user, course.id)
if enrollment is None or enrollment.mode != CourseMode.AUDIT:
return None
# We reset schedule.start in order to change a user's computed deadlines.
# But their expiration date shouldn't change when we adjust their schedule (they don't
# get additional time), so we need to based the expiration date on a fixed start date.
content_availability_date = max(enrollment.created, course.start)
return content_availability_date + access_duration
def check_course_expired(user, course):
"""
Check if the course expired for the user.
"""
# masquerading course staff should always have access
if get_course_masquerade(user, course.id):
return ACCESS_GRANTED
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
def get_date_string():
# Creating this method to allow unit testing an issue where this string was missing the unicode prefix
return u'{formatted_date_localized}'
def generate_course_expired_message(user, course):
"""
Generate the message for the user course expiration date if it exists.
"""
expiration_date = get_user_course_expiration_date(user, course)
if not expiration_date:
return
user_timezone_locale = user_timezone_locale_prefs(crum.get_current_request())
user_timezone = user_timezone_locale['user_timezone']
now = timezone.now()
if is_masquerading_as_specific_student(user, course.id) and now > expiration_date:
upgrade_message = _('This learner does not have access to this course. '
u'Their access expired on {expiration_date}.')
return HTML(upgrade_message).format(
expiration_date=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
)
else:
enrollment = CourseEnrollment.get_enrollment(user, course.id)
if enrollment is None:
return
upgrade_deadline = enrollment.upgrade_deadline
if (not upgrade_deadline) or (upgrade_deadline < now):
upgrade_deadline = enrollment.course_upgrade_deadline
expiration_message = _(u'{strong_open}Audit Access Expires {expiration_date}{strong_close}'
u'{line_break}You lose all access to this course, including your progress, on '
u'{expiration_date}.')
upgrade_deadline_message = _(u'{line_break}Upgrade by {upgrade_deadline} to get unlimited access to the course '
u'as long as it exists on the site. {a_open}Upgrade now{sronly_span_open} to '
u'retain access past {expiration_date}{span_close}{a_close}')
full_message = expiration_message
if upgrade_deadline and now < upgrade_deadline:
full_message += upgrade_deadline_message
using_upgrade_messaging = True
else:
using_upgrade_messaging = False
language = get_language()
date_string = get_date_string()
formatted_expiration_date = date_string.format(
language=language,
user_timezone=user_timezone,
formatted_date=expiration_date.isoformat(),
formatted_date_localized=strftime_localized(expiration_date, EXPIRATION_DATE_FORMAT_STR)
)
if using_upgrade_messaging:
formatted_upgrade_deadline = date_string.format(
language=language,
user_timezone=user_timezone,
formatted_date=upgrade_deadline.isoformat(),
formatted_date_localized=strftime_localized(upgrade_deadline, EXPIRATION_DATE_FORMAT_STR)
)
return HTML(full_message).format(
a_open=HTML(u'').format(
upgrade_link=verified_upgrade_deadline_link(user=user, course=course)
),
sronly_span_open=HTML(''),
span_close=HTML(''),
a_close=HTML(''),
expiration_date=HTML(formatted_expiration_date),
strong_open=HTML(''),
strong_close=HTML(''),
line_break=HTML('
'),
upgrade_deadline=HTML(formatted_upgrade_deadline)
)
else:
return HTML(full_message).format(
span_close=HTML(''),
expiration_date=HTML(formatted_expiration_date),
strong_open=HTML(''),
strong_close=HTML(''),
line_break=HTML('
'),
)
def generate_course_expired_fragment(user, course):
message = generate_course_expired_message(user, course)
if message:
return generate_fragment_from_message(message)
def generate_fragment_from_message(message):
return Fragment(HTML(u"""\