feat!: drop legacy course home view and related code
This was the "outline tab" view of the course. Preceded by the
course info view, succeeded by the MFE outline tab.
In addition to the course home view itself, this drops related
features:
- Legacy version of Course Goals (MFE has a newer implementation)
- Course home in-course search (MFE has no search)
The old course info view and course about views survive for now.
This also drops a few now-unused feature toggles:
- course_experience.latest_update
- course_experience.show_upgrade_msg_on_course_home
- course_experience.upgrade_deadline_message
- course_home.course_home_use_legacy_frontend
With this change, just the progress and courseware tabs are still
supported in legacy form, if you opt-in with waffle flags. The
outline and dates tabs are offered only by the MFE.
AA-798
(This is identical to previous commit be5c1a6, just reintroduced
now that the e2e tests have been fixed)
This commit is contained in:
@@ -3,11 +3,7 @@ Fragment for rendering the course dates sidebar.
|
||||
"""
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import get_language_bidi
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
@@ -44,35 +40,3 @@ class CourseDatesFragmentView(EdxFragmentView):
|
||||
self.add_fragment_resource_urls(dates_fragment)
|
||||
|
||||
return dates_fragment
|
||||
|
||||
|
||||
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
||||
class CourseDatesFragmentMobileView(CourseDatesFragmentView):
|
||||
"""
|
||||
A course dates fragment to show dates on mobile apps.
|
||||
|
||||
Mobile apps uses WebKit mobile client to create and maintain a session with
|
||||
the server for authenticated requests, and it hasn't exposed any way to find
|
||||
out either session was created with the server or not so mobile app uses a
|
||||
mechanism to automatically create/recreate session with the server for all
|
||||
authenticated requests if the server returns 404.
|
||||
"""
|
||||
template_name = 'course_experience/mobile/course-dates-fragment.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
raise Http404
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def css_dependencies(self):
|
||||
"""
|
||||
Returns list of CSS files that this view depends on.
|
||||
|
||||
The helper function that it uses to obtain the list of CSS files
|
||||
works in conjunction with the Django pipeline to ensure that in development mode
|
||||
the files are loaded individually, but in production just the single bundle is loaded.
|
||||
"""
|
||||
if get_language_bidi():
|
||||
return self.get_css_dependencies('style-mobile-rtl')
|
||||
else:
|
||||
return self.get_css_dependencies('style-mobile')
|
||||
|
||||
@@ -2,243 +2,13 @@
|
||||
Views for the course home page.
|
||||
"""
|
||||
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.context_processors import csrf
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import can_self_enroll_in_course, get_course_info_section, get_course_with_access
|
||||
from lms.djangoapps.course_goals.api import (
|
||||
get_course_goal,
|
||||
get_course_goal_options,
|
||||
get_goal_api_url,
|
||||
has_course_goal_permission
|
||||
)
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
|
||||
from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.views.views import CourseTabView
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_fragment
|
||||
from openedx.features.course_experience import (
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
LATEST_UPDATE_FLAG,
|
||||
SHOW_UPGRADE_MSG_ON_COURSE_HOME,
|
||||
)
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
|
||||
from openedx.features.course_experience.utils import get_course_outline_block_tree, get_resume_block, get_start_block
|
||||
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
|
||||
from openedx.features.course_experience.views.course_home_messages import CourseHomeMessageFragmentView
|
||||
from openedx.features.course_experience.views.course_outline import CourseOutlineFragmentView
|
||||
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
|
||||
from openedx.features.course_experience.views.latest_update import LatestUpdateFragmentView
|
||||
from openedx.features.course_experience.views.welcome_message import WelcomeMessageFragmentView
|
||||
from openedx.features.discounts.utils import format_strikeout_price
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.util.views import ensure_valid_course_key
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
EMPTY_HANDOUTS_HTML = '<ol></ol>'
|
||||
from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url
|
||||
|
||||
|
||||
class CourseHomeView(CourseTabView):
|
||||
"""
|
||||
The home page for a course.
|
||||
"""
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
|
||||
@method_decorator(ensure_valid_course_key)
|
||||
@method_decorator(add_maintenance_banner)
|
||||
def get(self, request, course_id, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Displays the home page for the specified course.
|
||||
"""
|
||||
return super().get(request, course_id, 'courseware', **kwargs)
|
||||
|
||||
def render_to_fragment(self, request, course=None, tab=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
|
||||
course_id = str(course.id)
|
||||
if course_home_legacy_is_active(course.id) or request.user.is_staff:
|
||||
home_fragment_view = CourseHomeFragmentView()
|
||||
return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
microfrontend_url = get_learning_mfe_home_url(course_key=course_id, url_fragment='home', params=request.GET)
|
||||
raise Redirect(microfrontend_url)
|
||||
|
||||
|
||||
class CourseHomeFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment to render the home page for a course.
|
||||
"""
|
||||
|
||||
def _get_resume_course_info(self, request, course_id):
|
||||
"""
|
||||
Returns information relevant to resume course functionality.
|
||||
|
||||
Returns a tuple: (has_visited_course, resume_course_url)
|
||||
has_visited_course: True if the user has ever completed a block, False otherwise.
|
||||
resume_course_url: The URL of the 'resume course' block if the user has completed a block,
|
||||
otherwise the URL of the first block to start the course.
|
||||
|
||||
"""
|
||||
course_outline_root_block = get_course_outline_block_tree(request, course_id, request.user)
|
||||
resume_block = get_resume_block(course_outline_root_block) if course_outline_root_block else None
|
||||
has_visited_course = bool(resume_block)
|
||||
if resume_block:
|
||||
resume_course_url = resume_block['lms_web_url']
|
||||
else:
|
||||
start_block = get_start_block(course_outline_root_block) if course_outline_root_block else None
|
||||
resume_course_url = start_block['lms_web_url'] if start_block else None
|
||||
|
||||
return has_visited_course, resume_course_url
|
||||
|
||||
def _get_course_handouts(self, request, course):
|
||||
"""
|
||||
Returns the handouts for the specified course.
|
||||
"""
|
||||
handouts = get_course_info_section(request, request.user, course, 'handouts')
|
||||
if not handouts or handouts == EMPTY_HANDOUTS_HTML:
|
||||
return None
|
||||
return handouts
|
||||
|
||||
def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, too-many-statements
|
||||
"""
|
||||
Renders the course's home page as a fragment.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# Render the course dates as a fragment
|
||||
dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
|
||||
# Render the full content to enrolled users, as well as to course and global staff.
|
||||
# Unenrolled users who are not course or global staff are given only a subset.
|
||||
enrollment = CourseEnrollment.get_enrollment(request.user, course_key)
|
||||
user_access = {
|
||||
'is_anonymous': request.user.is_anonymous,
|
||||
'is_enrolled': enrollment and enrollment.is_active,
|
||||
'is_staff': has_access(request.user, 'staff', course_key),
|
||||
}
|
||||
|
||||
allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key)
|
||||
allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC
|
||||
allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE
|
||||
|
||||
# Set all the fragments
|
||||
outline_fragment = None
|
||||
update_message_fragment = None
|
||||
course_sock_fragment = None
|
||||
course_expiration_fragment = None
|
||||
has_visited_course = None
|
||||
resume_course_url = None
|
||||
handouts_html = None
|
||||
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
if user_access['is_enrolled'] or user_access['is_staff']:
|
||||
outline_fragment = CourseOutlineFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
)
|
||||
if LATEST_UPDATE_FLAG.is_enabled(course_key):
|
||||
update_message_fragment = LatestUpdateFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
)
|
||||
else:
|
||||
update_message_fragment = WelcomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
)
|
||||
course_sock_fragment = CourseSockFragmentView().render_to_fragment(
|
||||
request, course=course, **kwargs
|
||||
)
|
||||
has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id)
|
||||
handouts_html = self._get_course_handouts(request, course)
|
||||
course_expiration_fragment = generate_course_expired_fragment(
|
||||
request.user,
|
||||
course_overview
|
||||
)
|
||||
elif allow_public_outline or allow_public:
|
||||
outline_fragment = CourseOutlineFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, user_is_enrolled=False, **kwargs
|
||||
)
|
||||
course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
|
||||
if allow_public:
|
||||
handouts_html = self._get_course_handouts(request, course)
|
||||
else:
|
||||
# Redirect the user to the dashboard if they are not enrolled and
|
||||
# this is a course that does not support direct enrollment.
|
||||
if not can_self_enroll_in_course(course_key):
|
||||
raise CourseAccessRedirect(reverse('dashboard'))
|
||||
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
# Check if the user can access the course goal functionality
|
||||
has_goal_permission = has_course_goal_permission(request, course_id, user_access)
|
||||
|
||||
# Grab the current course goal and the acceptable course goal keys mapped to translated values
|
||||
current_goal = get_course_goal(request.user, course_key)
|
||||
goal_options = get_course_goal_options()
|
||||
|
||||
# Get the course goals api endpoint
|
||||
goal_api_url = get_goal_api_url(request)
|
||||
|
||||
# Grab the course home messages fragment to render any relevant django messages
|
||||
course_home_message_fragment = CourseHomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, user_access=user_access, **kwargs
|
||||
)
|
||||
|
||||
# Get info for upgrade messaging
|
||||
upgrade_price = None
|
||||
upgrade_url = None
|
||||
has_discount = False
|
||||
|
||||
# TODO Add switch to control deployment
|
||||
if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled(course_key) and can_show_verified_upgrade(
|
||||
request.user,
|
||||
enrollment,
|
||||
course
|
||||
):
|
||||
upgrade_url = verified_upgrade_deadline_link(request.user, course_id=course_key)
|
||||
upgrade_price, has_discount = format_strikeout_price(request.user, course_overview)
|
||||
|
||||
show_search = (
|
||||
settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or
|
||||
(settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and user_access['is_staff'])
|
||||
)
|
||||
# Render the course home fragment
|
||||
context = {
|
||||
'request': request,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
'course_key': course_key,
|
||||
'outline_fragment': outline_fragment,
|
||||
'handouts_html': handouts_html,
|
||||
'course_home_message_fragment': course_home_message_fragment,
|
||||
'course_expiration_fragment': course_expiration_fragment,
|
||||
'has_visited_course': has_visited_course,
|
||||
'resume_course_url': resume_course_url,
|
||||
'course_tools': course_tools,
|
||||
'dates_fragment': dates_fragment,
|
||||
'username': request.user.username,
|
||||
'goal_api_url': goal_api_url,
|
||||
'has_goal_permission': has_goal_permission,
|
||||
'goal_options': goal_options,
|
||||
'current_goal': current_goal,
|
||||
'update_message_fragment': update_message_fragment,
|
||||
'course_sock_fragment': course_sock_fragment,
|
||||
'disable_courseware_js': True,
|
||||
'uses_bootstrap': True,
|
||||
'upgrade_price': upgrade_price,
|
||||
'upgrade_url': upgrade_url,
|
||||
'has_discount': has_discount,
|
||||
'show_search': show_search,
|
||||
}
|
||||
html = render_to_string('course_experience/course-home-fragment.html', context)
|
||||
return Fragment(html)
|
||||
@ensure_valid_course_key
|
||||
def outline_tab(request, course_id):
|
||||
"""Simply redirects to the MFE outline tab, as this legacy view for the course home/outline no longer exists."""
|
||||
return redirect(get_learning_mfe_home_url(course_key=course_id, url_fragment='home', params=request.GET))
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
"""
|
||||
View logic for handling course messages.
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from babel.dates import format_date, format_timedelta
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import get_language, to_locale
|
||||
from django.utils.translation import gettext as _
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
|
||||
from lms.djangoapps.course_goals.api import (
|
||||
get_course_goal,
|
||||
get_course_goal_options,
|
||||
get_goal_api_url,
|
||||
has_course_goal_permission,
|
||||
valid_course_goals_ordered
|
||||
)
|
||||
from lms.djangoapps.course_goals.models import GOAL_KEY_CHOICES
|
||||
from lms.djangoapps.courseware.access_utils import check_public_access
|
||||
from lms.djangoapps.courseware.toggles import course_is_invitation_only
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.course_experience import CourseHomeMessages
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment that displays a course message with an alert and call
|
||||
to action for three types of users:
|
||||
|
||||
1) Not logged in users are given a link to sign in or register.
|
||||
2) Unenrolled users are given a link to enroll.
|
||||
3) Enrolled users who get to the page before the course start date
|
||||
are given the option to add the start date to their calendar.
|
||||
|
||||
This fragment requires a user_access map as follows:
|
||||
|
||||
user_access = {
|
||||
'is_anonymous': True if the user is logged in, False otherwise
|
||||
'is_enrolled': True if the user is enrolled in the course, False otherwise
|
||||
'is_staff': True if the user is a staff member of the course, False otherwise
|
||||
}
|
||||
"""
|
||||
def render_to_fragment(self, request, course_id, user_access, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Renders a course message fragment for the specified course.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# Get time until the start date, if already started, or no start date, value will be zero or negative
|
||||
now = datetime.now(UTC)
|
||||
already_started = course.start and now > course.start
|
||||
days_until_start_string = "started" if already_started else format_timedelta(
|
||||
course.start - now, locale=to_locale(get_language())
|
||||
)
|
||||
course_start_data = {
|
||||
'course_start_date': format_date(course.start, locale=to_locale(get_language())),
|
||||
'already_started': already_started,
|
||||
'days_until_start_string': days_until_start_string
|
||||
}
|
||||
|
||||
# Register the course home messages to be loaded on the page
|
||||
_register_course_home_messages(request, course, user_access, course_start_data)
|
||||
|
||||
# Register course date alerts
|
||||
for course_date_block in get_course_date_blocks(course, request.user, request):
|
||||
course_date_block.register_alerts(request, course)
|
||||
|
||||
# Register a course goal message, if appropriate
|
||||
# Only show the set course goal message for enrolled, unverified
|
||||
# users that have not yet set a goal in a course that allows for
|
||||
# verified statuses.
|
||||
user_goal = get_course_goal(auth.get_user(request), course_key)
|
||||
is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
|
||||
if has_course_goal_permission(request, course_id, user_access) and not is_already_verified and not user_goal:
|
||||
_register_course_goal_message(request, course)
|
||||
|
||||
# Grab the relevant messages
|
||||
course_home_messages = list(CourseHomeMessages.user_messages(request))
|
||||
|
||||
# Pass in the url used to set a course goal
|
||||
goal_api_url = get_goal_api_url(request)
|
||||
|
||||
# Grab the logo
|
||||
image_src = 'course_experience/images/home_message_author.png'
|
||||
|
||||
context = {
|
||||
'course_home_messages': course_home_messages,
|
||||
'goal_api_url': goal_api_url,
|
||||
'image_src': image_src,
|
||||
'course_id': course_id,
|
||||
'username': request.user.username,
|
||||
}
|
||||
|
||||
html = render_to_string('course_experience/course-messages-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
|
||||
def _register_course_home_messages(request, course, user_access, course_start_data): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Register messages to be shown in the course home content page.
|
||||
"""
|
||||
allow_anonymous = check_public_access(course, [COURSE_VISIBILITY_PUBLIC])
|
||||
|
||||
if user_access['is_anonymous'] and not allow_anonymous:
|
||||
sign_in_or_register_text = (_('{sign_in_link} or {register_link} and then enroll in this course.')
|
||||
if not CourseMode.is_masters_only(course.id)
|
||||
else _('{sign_in_link} or {register_link}.'))
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(sign_in_or_register_text).format(
|
||||
sign_in_link=HTML('<a href="/login?next={current_url}">{sign_in_label}</a>').format(
|
||||
sign_in_label=_('Sign in'),
|
||||
current_url=quote_plus(request.path),
|
||||
),
|
||||
register_link=HTML('<a href="/register?next={current_url}">{register_label}</a>').format(
|
||||
register_label=_('register'),
|
||||
current_url=quote_plus(request.path),
|
||||
)
|
||||
),
|
||||
title=Text(_('You must be enrolled in the course to see course content.'))
|
||||
)
|
||||
if not user_access['is_anonymous'] and not user_access['is_staff'] and \
|
||||
not user_access['is_enrolled']:
|
||||
|
||||
title = Text(_('Welcome to {course_display_name}')).format(
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
|
||||
if CourseMode.is_masters_only(course.id):
|
||||
# if a course is a Master's only course, we will not offer user ability to self-enroll
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
'You must be enrolled in the course to see course content. '
|
||||
'Please contact your degree administrator or {platform_name} Support if you have questions.'
|
||||
)).format(platform_name=settings.PLATFORM_NAME),
|
||||
title=title
|
||||
)
|
||||
elif not course_is_invitation_only(course):
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
'{open_enroll_link}Enroll now{close_enroll_link} to access the full course.'
|
||||
)).format(
|
||||
open_enroll_link=HTML('<button class="enroll-btn btn-link">'),
|
||||
close_enroll_link=HTML('</button>')
|
||||
),
|
||||
title=title
|
||||
)
|
||||
else:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_('You must be enrolled in the course to see course content.')),
|
||||
)
|
||||
|
||||
|
||||
def _register_course_goal_message(request, course):
|
||||
"""
|
||||
Register a message to let a learner specify a course goal.
|
||||
"""
|
||||
course_goal_options = get_course_goal_options()
|
||||
goal_choices_html = Text(_(
|
||||
'To start, set a course goal by selecting the option below that best describes '
|
||||
'your learning plan. {goal_options_container}'
|
||||
)).format(
|
||||
goal_options_container=HTML('<div class="row goal-options-container">')
|
||||
)
|
||||
|
||||
# Add the dismissible option for users that are unsure of their goal
|
||||
goal_choices_html += Text(
|
||||
'{initial_tag}{choice}{closing_tag}'
|
||||
).format(
|
||||
initial_tag=HTML(
|
||||
'<div tabindex="0" aria-label="{aria_label_choice}" class="goal-option dismissible" '
|
||||
'data-choice="{goal_key}">'
|
||||
).format(
|
||||
goal_key=GOAL_KEY_CHOICES.unsure,
|
||||
aria_label_choice=Text(_("Set goal to: {choice}")).format(
|
||||
choice=course_goal_options[GOAL_KEY_CHOICES.unsure],
|
||||
),
|
||||
),
|
||||
choice=Text(_('{choice}')).format(
|
||||
choice=course_goal_options[GOAL_KEY_CHOICES.unsure],
|
||||
),
|
||||
closing_tag=HTML('</div>'),
|
||||
)
|
||||
|
||||
# Add the option to set a goal to earn a certificate,
|
||||
# complete the course or explore the course
|
||||
course_goals_by_commitment_level = valid_course_goals_ordered()
|
||||
for goal in course_goals_by_commitment_level:
|
||||
goal_key, goal_text = goal
|
||||
goal_choices_html += HTML(
|
||||
'{initial_tag}{goal_text}{closing_tag}'
|
||||
).format(
|
||||
initial_tag=HTML(
|
||||
'<button tabindex="0" aria-label="{aria_label_choice}" class="goal-option btn-outline-primary" '
|
||||
'data-choice="{goal_key}">'
|
||||
).format(
|
||||
goal_key=goal_key,
|
||||
aria_label_choice=Text(_("Set goal to: {goal_text}")).format(
|
||||
goal_text=Text(_(goal_text)) # lint-amnesty, pylint: disable=translation-of-non-string
|
||||
)
|
||||
),
|
||||
goal_text=goal_text,
|
||||
closing_tag=HTML('</button>')
|
||||
)
|
||||
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
HTML('{goal_choices_html}{closing_tag}').format(
|
||||
goal_choices_html=goal_choices_html,
|
||||
closing_tag=HTML('</div>')
|
||||
),
|
||||
title=Text(_('Welcome to {course_display_name}')).format(
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
)
|
||||
@@ -1,165 +0,0 @@
|
||||
"""
|
||||
Views to show a course outline.
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.template.context_processors import csrf
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
import edx_when.api as edx_when_api
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from waffle.models import Switch
|
||||
from web_fragments.fragment import Fragment
|
||||
from lms.djangoapps.courseware.courses import get_course_overview_with_access
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience.utils import dates_banner_should_display
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.util.milestones_helpers import get_course_content_milestones
|
||||
from xmodule.course_module import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from ..utils import get_course_outline_block_tree, get_resume_block
|
||||
|
||||
DEFAULT_COMPLETION_TRACKING_START = datetime.datetime(2018, 1, 24, tzinfo=UTC)
|
||||
|
||||
|
||||
class CourseOutlineFragmentView(EdxFragmentView):
|
||||
"""
|
||||
Course outline fragment to be shown in the unified course view.
|
||||
"""
|
||||
|
||||
def render_to_fragment(self, request, course_id, user_is_enrolled=True, **kwargs): # pylint: disable=arguments-differ
|
||||
"""
|
||||
Renders the course outline as a fragment.
|
||||
"""
|
||||
from lms.urls import RESET_COURSE_DEADLINES_NAME
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course_overview = get_course_overview_with_access(
|
||||
request.user, 'load', course_key, check_if_enrolled=user_is_enrolled
|
||||
)
|
||||
course = modulestore().get_course(course_key)
|
||||
|
||||
course_block_tree = get_course_outline_block_tree(
|
||||
request, course_id, request.user if user_is_enrolled else None
|
||||
)
|
||||
if not course_block_tree:
|
||||
return None
|
||||
|
||||
resume_block = get_resume_block(course_block_tree) if user_is_enrolled else None
|
||||
|
||||
if not resume_block:
|
||||
self.mark_first_unit_to_resume(course_block_tree)
|
||||
|
||||
xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
|
||||
gated_content = self.get_content_milestones(request, course_key)
|
||||
|
||||
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
|
||||
|
||||
reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME)
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course_overview,
|
||||
'due_date_display_format': course.due_date_display_format,
|
||||
'blocks': course_block_tree,
|
||||
'enable_links': user_is_enrolled or course.course_visibility == COURSE_VISIBILITY_PUBLIC,
|
||||
'course_key': course_key,
|
||||
'gated_content': gated_content,
|
||||
'xblock_display_names': xblock_display_names,
|
||||
'self_paced': course.self_paced,
|
||||
|
||||
# We're using this flag to prevent old self-paced dates from leaking out on courses not
|
||||
# managed by edx-when.
|
||||
'in_edx_when': edx_when_api.is_enabled_for_course(course_key),
|
||||
'reset_deadlines_url': reset_deadlines_url,
|
||||
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
|
||||
'on_course_outline_page': True,
|
||||
'missed_deadlines': missed_deadlines,
|
||||
'missed_gated_content': missed_gated_content,
|
||||
'has_ended': course.has_ended(),
|
||||
}
|
||||
|
||||
html = render_to_string('course_experience/course-outline-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
def create_xblock_id_and_name_dict(self, course_block_tree, xblock_display_names=None):
|
||||
"""
|
||||
Creates a dictionary mapping xblock IDs to their names, using a course block tree.
|
||||
"""
|
||||
if xblock_display_names is None:
|
||||
xblock_display_names = {}
|
||||
|
||||
if not course_block_tree.get('authorization_denial_reason'):
|
||||
if course_block_tree.get('id'):
|
||||
xblock_display_names[course_block_tree['id']] = course_block_tree['display_name']
|
||||
|
||||
if course_block_tree.get('children'):
|
||||
for child in course_block_tree['children']:
|
||||
self.create_xblock_id_and_name_dict(child, xblock_display_names)
|
||||
|
||||
return xblock_display_names
|
||||
|
||||
def get_content_milestones(self, request, course_key):
|
||||
"""
|
||||
Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
|
||||
"""
|
||||
def _get_key_of_prerequisite(namespace):
|
||||
return re.sub('.gating', '', namespace)
|
||||
|
||||
all_course_milestones = get_course_content_milestones(course_key)
|
||||
|
||||
uncompleted_prereqs = {
|
||||
milestone['content_id']
|
||||
for milestone in get_course_content_milestones(course_key, user_id=request.user.id)
|
||||
}
|
||||
|
||||
gated_content = {
|
||||
milestone['content_id']: {
|
||||
'completed_prereqs': milestone['content_id'] not in uncompleted_prereqs,
|
||||
'prerequisite': _get_key_of_prerequisite(milestone['namespace'])
|
||||
}
|
||||
for milestone in all_course_milestones
|
||||
}
|
||||
|
||||
return gated_content
|
||||
|
||||
def user_enrolled_after_completion_collection(self, user, course_key):
|
||||
"""
|
||||
Checks that the user has enrolled in the course after 01/24/2018, the date that
|
||||
the completion API began data collection. If the user has enrolled in the course
|
||||
before this date, they may see incomplete collection data. This is a temporary
|
||||
check until all active enrollments are created after the date.
|
||||
"""
|
||||
user = User.objects.get(username=user)
|
||||
try:
|
||||
user_enrollment = CourseEnrollment.objects.get(
|
||||
user=user,
|
||||
course_id=course_key,
|
||||
is_active=True
|
||||
)
|
||||
return user_enrollment.created > self._completion_data_collection_start()
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return False
|
||||
|
||||
def _completion_data_collection_start(self):
|
||||
"""
|
||||
Returns the date that the ENABLE_COMPLETION_TRACKING waffle switch was enabled.
|
||||
"""
|
||||
try:
|
||||
return Switch.objects.get(name=ENABLE_COMPLETION_TRACKING_SWITCH.name).created
|
||||
except Switch.DoesNotExist:
|
||||
return DEFAULT_COMPLETION_TRACKING_START
|
||||
|
||||
def mark_first_unit_to_resume(self, block_node):
|
||||
children = block_node.get('children')
|
||||
if children:
|
||||
children[0]['resume_block'] = True
|
||||
self.mark_first_unit_to_resume(children[0])
|
||||
@@ -2,11 +2,9 @@
|
||||
Views that handle course updates.
|
||||
"""
|
||||
|
||||
import six # lint-amnesty, pylint: disable=unused-import
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template.context_processors import csrf
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -15,7 +13,7 @@ from web_fragments.fragment import Fragment
|
||||
from lms.djangoapps.courseware.courses import get_course_info_section_module, get_course_with_access
|
||||
from lms.djangoapps.courseware.views.views import CourseTabView
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience import default_course_url_name
|
||||
from openedx.features.course_experience import default_course_url
|
||||
from openedx.features.course_experience.course_updates import get_ordered_updates
|
||||
|
||||
|
||||
@@ -48,8 +46,7 @@ class CourseUpdatesFragmentView(EdxFragmentView):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_url_name = default_course_url_name(course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': str(course.id)})
|
||||
course_url = default_course_url(course.id)
|
||||
|
||||
ordered_updates = get_ordered_updates(request, course)
|
||||
plain_html_updates = ''
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
"""
|
||||
View logic for handling latest course updates.
|
||||
|
||||
Although the welcome message fragment also displays the latest update,
|
||||
this fragment dismisses the message for a limited time so new updates
|
||||
will continue to appear, where the welcome message gets permanently
|
||||
dismissed.
|
||||
"""
|
||||
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience.course_updates import get_current_update_for_user
|
||||
|
||||
|
||||
class LatestUpdateFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment that displays the latest course update.
|
||||
"""
|
||||
|
||||
def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Renders the latest update message fragment for the specified course.
|
||||
|
||||
Returns: A fragment, or None if there is no latest update message.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
|
||||
update_html = self.latest_update_html(request, course)
|
||||
if not update_html:
|
||||
return None
|
||||
|
||||
context = {
|
||||
'update_html': update_html,
|
||||
}
|
||||
html = render_to_string('course_experience/latest-update-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
@classmethod
|
||||
def latest_update_html(cls, request, course):
|
||||
"""
|
||||
Returns the course's latest update message or None if it doesn't have one.
|
||||
"""
|
||||
# Return the course update with the most recent publish date
|
||||
return get_current_update_for_user(request, course)
|
||||
@@ -1,67 +0,0 @@
|
||||
""" # lint-amnesty, pylint: disable=cyclic-import
|
||||
View logic for handling course welcome messages.
|
||||
"""
|
||||
|
||||
|
||||
import six # lint-amnesty, pylint: disable=unused-import
|
||||
from django.http import HttpResponse
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience.course_updates import (
|
||||
dismiss_current_update_for_user, get_current_update_for_user,
|
||||
)
|
||||
|
||||
|
||||
class WelcomeMessageFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment that displays a course's welcome message.
|
||||
"""
|
||||
|
||||
def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Renders the welcome message fragment for the specified course.
|
||||
|
||||
Returns: A fragment, or None if there is no welcome message.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
welcome_message_html = self.welcome_message_html(request, course)
|
||||
if not welcome_message_html:
|
||||
return None
|
||||
|
||||
dismiss_url = reverse(
|
||||
'openedx.course_experience.dismiss_welcome_message', kwargs={'course_id': str(course_key)}
|
||||
)
|
||||
|
||||
context = {
|
||||
'dismiss_url': dismiss_url,
|
||||
'welcome_message_html': welcome_message_html,
|
||||
}
|
||||
|
||||
html = render_to_string('course_experience/welcome-message-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
@classmethod
|
||||
def welcome_message_html(cls, request, course):
|
||||
"""
|
||||
Returns the course's welcome message or None if it doesn't have one.
|
||||
"""
|
||||
# Return the course update with the most recent publish date
|
||||
return get_current_update_for_user(request, course)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def dismiss_welcome_message(request, course_id):
|
||||
"""
|
||||
Given the course_id in the request, disable displaying the welcome message for the user.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
dismiss_current_update_for_user(request, course)
|
||||
return HttpResponse()
|
||||
Reference in New Issue
Block a user