The announcements editor was never ported to frontend-app-authoring, and the announcements display was never ported to frontend-app-learner-dashboard. This announcements feature is rarely used, undocumented, non-a11y-friendly, and there were no volunteers to port it to the new frontends. It is the last remaining part of the legacy Studio "Maintenance" dashboard. So, we are removing it. BREAKING CHANGE: This removes... * Studio Maintenance dashboard legacy frontend * Studio Edit Announcements legacy frontend * The snippet of legacy learner dashboard which renders announcements * openedx/features/announcements djangoapp * The ENABLE_ANNOUNCEMENTS feature flag Not removed: * The announcements_announcement table from openedx/features/announcements . The table is most likely very small, as it is only populated by administrators. Removing it would be more labor for us and more risk of toil for operators than is worthwhile. Closes: https://github.com/openedx/edx-platform/issues/36263
394 lines
20 KiB
HTML
394 lines
20 KiB
HTML
<%page expression_filter="h"/>
|
||
<%inherit file="main.html" />
|
||
<%def name="online_help_token()"><% return "learnerdashboard" %></%def>
|
||
<%namespace name='static' file='static_content.html'/>
|
||
<%!
|
||
import pytz
|
||
from datetime import datetime, timedelta
|
||
from django.urls import reverse
|
||
from django.utils.translation import gettext as _
|
||
from django.template import RequestContext
|
||
from common.djangoapps.entitlements.models import CourseEntitlement
|
||
from common.djangoapps.third_party_auth import pipeline
|
||
from common.djangoapps.util.date_utils import strftime_localized
|
||
from opaque_keys.edx.keys import CourseKey
|
||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||
from openedx.core.djangolib.markup import HTML, Text
|
||
|
||
from common.djangoapps.student.models import CourseEnrollment
|
||
%>
|
||
|
||
<%
|
||
cert_name_short = settings.CERT_NAME_SHORT
|
||
cert_name_long = settings.CERT_NAME_LONG
|
||
%>
|
||
|
||
|
||
<%block name="pagetitle">${_("Dashboard")}</%block>
|
||
<%block name="bodyclass">view-dashboard is-authenticated</%block>
|
||
|
||
<%block name="header_extras">
|
||
% for template_name in ["donation"]:
|
||
<script type="text/template" id="${template_name}-tpl">
|
||
<%static:include path="dashboard/${template_name}.underscore" />
|
||
</script>
|
||
% endfor
|
||
</%block>
|
||
|
||
<%block name="js_extra">
|
||
<script src="${static.url('js/commerce/credit.js')}"></script>
|
||
<script type="text/javascript" src="${static.url('js/learner_dashboard/certificate_api.js')}"></script>
|
||
<%static:js group='dashboard'/>
|
||
<script type="text/javascript">
|
||
$(document).ready(function() {
|
||
edx.dashboard.legacy.init({
|
||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||
signInUser: "${reverse('signin_user') | n, js_escaped_string}",
|
||
changeEmailSettings: "${reverse('change_email_settings') | n, js_escaped_string}",
|
||
sendAccountActivationEmail: "${reverse('send_account_activation_email') | n, js_escaped_string}"
|
||
|
||
});
|
||
});
|
||
</script>
|
||
<%static:webpack entry="UnenrollmentFactory">
|
||
UnenrollmentFactory({
|
||
urls: {
|
||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||
signInUser: "${reverse('signin_user') | n, js_escaped_string}",
|
||
changeEmailSettings: "${reverse('change_email_settings') | n, js_escaped_string}",
|
||
browseCourses: "${marketing_link('COURSES') | n, js_escaped_string}"
|
||
},
|
||
isEdx: false
|
||
});
|
||
</%static:webpack>
|
||
<%static:webpack entry="EntitlementUnenrollmentFactory">
|
||
## Wait until the document is fully loaded before initializing the EntitlementUnenrollmentView
|
||
## to ensure events are setup correctly.
|
||
$(document).ready(function() {
|
||
EntitlementUnenrollmentFactory({
|
||
dashboardPath: "${reverse('dashboard') | n, js_escaped_string}",
|
||
signInPath: "${reverse('signin_user') | n, js_escaped_string}",
|
||
browseCourses: "${marketing_link('COURSES') | n, js_escaped_string}",
|
||
isEdx: false
|
||
});
|
||
});
|
||
</%static:webpack>
|
||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
||
DashboardSearchFactory();
|
||
</%static:require_module>
|
||
% endif
|
||
% if redirect_message:
|
||
<%static:require_module module_name="js/views/message_banner" class_name="MessageBannerView">
|
||
var banner = new MessageBannerView({urgency: 'low', type: 'warning'});
|
||
$('#content').prepend(banner.$el);
|
||
banner.showMessage(${redirect_message | n, dump_js_escaped_json})
|
||
</%static:require_module>
|
||
% endif
|
||
% if recovery_email_message:
|
||
<%static:require_module module_name="js/views/message_banner" class_name="MessageBannerView">
|
||
var banner = new MessageBannerView({urgency: 'low', type: 'warning', hideCloseBtn: false, isRecoveryEmailMsg: true});
|
||
$('#content').prepend(banner.$el);
|
||
banner.showMessage(${recovery_email_message | n, dump_js_escaped_json})
|
||
</%static:require_module>
|
||
% endif
|
||
% if recovery_email_activation_message:
|
||
<%static:require_module module_name="js/views/message_banner" class_name="MessageBannerView">
|
||
var banner = new MessageBannerView({urgency: 'low', type: 'warning', isRecoveryEmailMsg: true});
|
||
$('#content').prepend(banner.$el);
|
||
banner.showMessage(${recovery_email_activation_message | n, dump_js_escaped_json})
|
||
</%static:require_module>
|
||
% endif
|
||
% if enterprise_learner_portal_enabled_message:
|
||
<%static:require_module module_name="js/views/message_banner" class_name="MessageBannerView">
|
||
var banner = new MessageBannerView({urgency: 'low', type: 'warning', isLearnerPortalEnabled: true});
|
||
$('#content').prepend(banner.$el);
|
||
banner.showMessage(${enterprise_learner_portal_enabled_message | n, dump_js_escaped_json})
|
||
</%static:require_module>
|
||
% endif
|
||
</%block>
|
||
|
||
<div class="dashboard-notifications" tabindex="-1">
|
||
|
||
%if banner_account_activation_message:
|
||
<div class="dashboard-banner">
|
||
${banner_account_activation_message | n, decode.utf8}
|
||
</div>
|
||
%endif
|
||
|
||
%if enrollment_message:
|
||
<div class="dashboard-banner">
|
||
${enrollment_message | n, decode.utf8}
|
||
</div>
|
||
%endif
|
||
|
||
%if enterprise_message:
|
||
<div class="dashboard-banner">
|
||
${ enterprise_message | n, decode.utf8 }
|
||
</div>
|
||
%endif
|
||
|
||
%if account_activation_messages:
|
||
<div class="activation-message-container">
|
||
% for account_activation_message in account_activation_messages:
|
||
<div class="account-activation ${account_activation_message.tags}" role="alert" aria-label="Account Activation Message" tabindex="-1">
|
||
<div class="message-copy" >
|
||
${ account_activation_message | n, decode.utf8 }
|
||
</div>
|
||
</div>
|
||
% endfor
|
||
</div>
|
||
%endif
|
||
|
||
</div>
|
||
|
||
<main id="main" aria-label="Content" tabindex="-1">
|
||
<div class="dashboard" id="dashboard-main">
|
||
<div class="main-container">
|
||
<div class="my-courses" id="my-courses">
|
||
% if display_dashboard_courses:
|
||
<%include file="learner_dashboard/_dashboard_navigation_courses.html"/>
|
||
% endif
|
||
|
||
% if len(course_entitlements + course_enrollments) > 0:
|
||
<ul class="listing-courses">
|
||
<%
|
||
share_settings = configuration_helpers.get_value(
|
||
'SOCIAL_SHARING_SETTINGS',
|
||
getattr(settings, 'SOCIAL_SHARING_SETTINGS', {})
|
||
)
|
||
%>
|
||
% for dashboard_index, enrollment in enumerate(course_entitlements + course_enrollments):
|
||
<%
|
||
# Check if the course run is an entitlement and if it has an associated session
|
||
entitlement = enrollment if isinstance(enrollment, CourseEntitlement) else None
|
||
entitlement_session = entitlement.enrollment_course_run if entitlement else None
|
||
entitlement_days_until_expiration = entitlement.get_days_until_expiration() if entitlement else None
|
||
entitlement_expiration = datetime.now(tz=pytz.UTC) + timedelta(days=entitlement_days_until_expiration) if (entitlement and entitlement_days_until_expiration < settings.ENTITLEMENT_EXPIRED_ALERT_PERIOD) else None
|
||
entitlement_expiration_date = strftime_localized(entitlement_expiration, 'SHORT_DATE') if entitlement and entitlement_expiration else None
|
||
entitlement_expired_at = strftime_localized(entitlement.expired_at_datetime, 'SHORT_DATE') if entitlement and entitlement.expired_at_datetime else None
|
||
|
||
is_fulfilled_entitlement = True if entitlement and entitlement_session else False
|
||
is_unfulfilled_entitlement = True if entitlement and not entitlement_session else False
|
||
|
||
entitlement_available_sessions = []
|
||
if entitlement:
|
||
# Grab the available, enrollable sessions for a given entitlement and scrape them for relevant attributes
|
||
entitlement_available_sessions = [{
|
||
'session_id': course['key'],
|
||
'enrollment_end': course['enrollment_end'],
|
||
'pacing_type': course['pacing_type'],
|
||
'advertised_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
|
||
'start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
|
||
'end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end,
|
||
} for course in course_entitlement_available_sessions[str(entitlement.uuid)]]
|
||
if is_fulfilled_entitlement:
|
||
# If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
|
||
enrollment = entitlement_session
|
||
else:
|
||
# If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object to populate card with metadata
|
||
pseudo_session = unfulfilled_entitlement_pseudo_sessions[str(entitlement.uuid)]
|
||
if not pseudo_session:
|
||
continue
|
||
pseudo_key = pseudo_session['key']
|
||
if not isinstance(pseudo_key, CourseKey):
|
||
pseudo_key = CourseKey.from_string(pseudo_session['key'])
|
||
enrollment = CourseEnrollment(user=user, course=CourseOverview.get_from_id(pseudo_key), mode=pseudo_session['type'])
|
||
# We only show email settings for entitlement cards if the entitlement has an associated enrollment
|
||
show_email_settings = is_fulfilled_entitlement and (entitlement_session.course_id in show_email_settings_for)
|
||
course_overview = enrollment.course_overview
|
||
else:
|
||
show_email_settings = (enrollment.course_id in show_email_settings_for)
|
||
course_overview = CourseOverview.get_from_id(enrollment.course_id)
|
||
|
||
session_id = enrollment.course_id
|
||
show_courseware_link = show_courseware_links_for.get(session_id, False)
|
||
cert_status = cert_statuses.get(session_id)
|
||
can_refund_entitlement = entitlement and entitlement.is_entitlement_refundable()
|
||
partner_managed_enrollment = enrollment.mode == 'masters'
|
||
# checks if we can unenroll based on the value of partner_managed_enrollment
|
||
can_unenroll_partner_managed_enrollment = False if partner_managed_enrollment else (not cert_status)
|
||
# checks if we can unenroll based on the value of unfulfilled_entitlement
|
||
can_unenroll_unfulfilled_entitlement = cert_status.get('can_unenroll') if cert_status and not unfulfilled_entitlement else False
|
||
# compares the three different parameters by which we can unenroll
|
||
can_unenroll = (can_unenroll_partner_managed_enrollment or can_unenroll_unfulfilled_entitlement) and not disable_unenrollment
|
||
credit_status = credit_statuses.get(session_id)
|
||
course_mode_info = all_course_modes.get(session_id)
|
||
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid)
|
||
is_course_voucher_refundable = (session_id in enrolled_courses_voucher_refundable)
|
||
course_requirements = courses_requirements_not_met.get(session_id)
|
||
related_programs = inverted_programs.get(str(entitlement.course_uuid if is_unfulfilled_entitlement else session_id))
|
||
show_consent_link = (session_id in consent_required_courses)
|
||
resume_button_url = resume_button_urls[dashboard_index]
|
||
%>
|
||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, enrollments_fbe_is_on=enrollments_fbe_is_on, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' />
|
||
% endfor
|
||
% if show_load_all_courses_link:
|
||
<br/>
|
||
${len(course_enrollments)} ${_("results successfully populated,")}
|
||
<a href="${reverse('dashboard')}?course_limit=None">
|
||
${_("Click to load all enrolled courses")}
|
||
</a>
|
||
% endif
|
||
</ul>
|
||
% else:
|
||
<div class="empty-dashboard-message">
|
||
% if display_dashboard_courses:
|
||
<p>${_("You are not enrolled in any courses yet.")}</p>
|
||
% if empty_dashboard_message:
|
||
<p class="custom-message">${empty_dashboard_message | n, decode.utf8}</p>
|
||
%endif
|
||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
|
||
<a class="btn btn-primary" href="${marketing_link('COURSES')}">
|
||
${_("Explore courses")}
|
||
</a>
|
||
%endif
|
||
% else:
|
||
<p>${_("Activate your account!")}</p>
|
||
<p class="custom-message">${ activate_account_message | n, decode.utf8 }</p>
|
||
% endif
|
||
</div>
|
||
% endif
|
||
|
||
% if staff_access and len(errored_courses) > 0:
|
||
<div id="course-errors">
|
||
<h2>${_("Course-loading errors")}</h2>
|
||
|
||
% for course_dir, errors in errored_courses.items():
|
||
<h3>${course_dir}</h3>
|
||
<ul>
|
||
% for (msg, err) in errors:
|
||
<li>${msg}
|
||
<ul><li><pre>${err}</pre></li></ul>
|
||
</li>
|
||
% endfor
|
||
</ul>
|
||
% endfor
|
||
</div>
|
||
% endif
|
||
</div>
|
||
</div>
|
||
<div class="side-container" role="complementary" aria-label="messages">
|
||
%if display_sidebar_account_activation_message:
|
||
<div class="sidebar-notification">
|
||
<%include file="${static.get_template_path('registration/account_activation_sidebar_notice.html')}" />
|
||
</div>
|
||
%endif
|
||
|
||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||
<div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard">
|
||
<form class="search-form">
|
||
<label for="dashboard-search-input">${_('Search Your Courses')}</label>
|
||
<div class="search-field-wrapper">
|
||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||
<button type="submit" class="search-button" title="${_('Search')}">
|
||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||
</button>
|
||
<button type="button" class="cancel-button" title="${_('Clear search')}">
|
||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div id="dashboard-search-results" class="search-results dashboard-search-results"></div>
|
||
% endif
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
%if show_account_activation_popup:
|
||
<div id="activate-account-modal" class="modal activate-account-modal" aria-hidden="true" tabindex=0 >
|
||
<div class="inner-wrapper" role="dialog" aria-labelledby="activate-account-modal-title" aria-live="polite">
|
||
<h3>
|
||
${_("Activate your account so you can log back in")}
|
||
<span class="sr">,
|
||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||
${_("window open")}
|
||
</span>
|
||
</h3>
|
||
<p class="activate-account-modal-body">${Text(_("We sent an email to {strong_start}{email}{strong_end} with a link to activate your account. Can’t find it? Check your spam folder or {link_start}resend the email{link_end}.")).format(
|
||
strong_start=HTML('<strong>'),
|
||
email=user.email,
|
||
strong_end=HTML('</strong>'),
|
||
link_start=HTML('<a href="#" id="send_cta_email" >'),
|
||
link_end=HTML('</a>')
|
||
)}
|
||
</p>
|
||
<div class="activate-account-modal-button">
|
||
<button class="btn btn-primary" id="button">
|
||
${Text(_("Continue to {platform_name}")).format(platform_name=settings.PLATFORM_NAME)}
|
||
<svg style="vertical-align:bottom" width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
%endif
|
||
|
||
<div id="email-settings-modal" class="modal" aria-hidden="true">
|
||
<div class="inner-wrapper" role="dialog" aria-labelledby="email-settings-title">
|
||
<button class="close-modal">
|
||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||
<span class="sr">
|
||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||
${_("Close")}
|
||
</span>
|
||
</button>
|
||
|
||
<header>
|
||
<h2 id="email-settings-title">
|
||
${Text(_("Email Settings for {course_number}")).format(course_number=HTML('<span id="email_settings_course_number"></span>'))}
|
||
<span class="sr">,
|
||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||
${_("window open")}
|
||
</span>
|
||
</h2>
|
||
<hr/>
|
||
</header>
|
||
|
||
<form id="email_settings_form" method="post">
|
||
<input name="course_id" id="email_settings_course_id" type="hidden" />
|
||
<label><input type="checkbox" id="receive_emails" name="receive_emails" />${_("Receive course emails")} </label>
|
||
<div class="submit">
|
||
<input type="submit" id="submit" value="${_("Save Settings")}" />
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
|
||
<div class="inner-wrapper" role="dialog" aria-labelledby="unenrollment-modal-title" aria-live="polite">
|
||
<button class="close-modal">
|
||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||
<span class="sr">
|
||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||
${_("Close")}
|
||
</span>
|
||
</button>
|
||
|
||
<header class="unenroll-header">
|
||
<h2 id="unenrollment-modal-title">
|
||
<span id='track-info'></span>
|
||
<span id='refund-info'></span>
|
||
<span class="sr">,
|
||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||
${_("window open")}
|
||
</span>
|
||
</h2>
|
||
<hr/>
|
||
</header>
|
||
<div id="unenroll_error" class="modal-form-error"></div>
|
||
<form id="unenroll_form" method="post" data-remote="true" action="${reverse('change_enrollment')}">
|
||
<input name="course_id" id="unenroll_course_id" type="hidden" />
|
||
<input name="enrollment_action" type="hidden" value="unenroll" />
|
||
<div class="submit">
|
||
<input class="submit-button" name="submit" type="submit" value="${_("Unenroll")}" />
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<%include file="dashboard/_dashboard_entitlement_unenrollment_modal.html"/>
|