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:
Michael Terry
2022-03-15 09:32:14 -04:00
parent 584f400ca8
commit ce5f1bb343
86 changed files with 194 additions and 5747 deletions

View File

@@ -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')

View File

@@ -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))

View File

@@ -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
)
)

View File

@@ -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])

View File

@@ -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 = ''

View File

@@ -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)

View File

@@ -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()