Merge pull request #30243 from openedx/aakbar/PROD-2739

feat: use new financial assistance flow in FinancialAssistanceTool
This commit is contained in:
Ali Akbar
2022-05-12 23:45:55 +05:00
committed by GitHub
8 changed files with 146 additions and 56 deletions

View File

@@ -2,7 +2,7 @@
Constants for courseware app.
"""
UNEXPECTED_ERROR_IS_ELIGIBLE = "An unexpected error occurred while fetching " \
"financial assistance eligibility criteria for a course"
"financial assistance eligibility criteria for this course"
UNEXPECTED_ERROR_APPLICATION_STATUS = "An unexpected error occurred while getting " \
"financial assistance application status"
UNEXPECTED_ERROR_CREATE_APPLICATION = "An unexpected error occurred while creating financial assistance application"

View File

@@ -7,12 +7,14 @@ import datetime
import pytz
from django.conf import settings
from django.utils.translation import gettext as _
from django.urls import reverse
from django.utils.translation import gettext as _
from common.djangoapps.course_modes.models import CourseMode
from openedx.features.course_experience.course_tools import CourseTool
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.courseware.utils import _use_new_financial_assistance_flow
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.features.course_experience.course_tools import CourseTool
class FinancialAssistanceTool(CourseTool):
@@ -86,4 +88,6 @@ class FinancialAssistanceTool(CourseTool):
"""
Returns the URL for this tool for the specified course key.
"""
if _use_new_financial_assistance_flow(str(course_key)):
return reverse('financial_assistance_v2', args=[course_key])
return reverse('financial_assistance')

View File

@@ -843,6 +843,17 @@ class ViewsTestCase(BaseViewsTestCase):
assert response.status_code == 200
self.assertContains(response, 'Financial Assistance Application')
@patch('lms.djangoapps.courseware.views.views._use_new_financial_assistance_flow', return_value=True)
@patch('lms.djangoapps.courseware.views.views.is_eligible_for_financial_aid', return_value=(False, 'error reason'))
def test_new_financial_assistance_page_course_ineligible(self, *args):
"""
Test to verify the financial_assistance view against an ineligible course returns an error page.
"""
url = reverse('financial_assistance_v2', args=['course-v1:test+TestX+Test_Course'])
response = self.client.get(url)
assert response.status_code == 200
self.assertContains(response, 'This course is not eligible for Financial Assistance for the following reason:')
@ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, YESTERDAY),
([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None),
([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None),
@@ -902,10 +913,11 @@ class ViewsTestCase(BaseViewsTestCase):
self.assertContains(response, str(course))
def _submit_financial_assistance_form(self, data, submit_url='submit_financial_assistance_request'):
def _submit_financial_assistance_form(self, data, submit_url='submit_financial_assistance_request',
referrer_url=None):
"""Submit a financial assistance request."""
url = reverse(submit_url)
return self.client.post(url, json.dumps(data), content_type='application/json')
return self.client.post(url, json.dumps(data), content_type='application/json', HTTP_REFERER=referrer_url)
@patch.object(views, 'create_zendesk_ticket', return_value=200)
def test_submit_financial_assistance_request(self, mock_create_zendesk_ticket):
@@ -968,7 +980,12 @@ class ViewsTestCase(BaseViewsTestCase):
@patch.object(
views, 'create_financial_assistance_application', return_value=HttpResponse(status=status.HTTP_204_NO_CONTENT)
)
def test_submit_financial_assistance_request_v2(self, create_application_mock):
@ddt.data(
('/financial-assistance/course-v1:test+TestX+Test_Course/apply/', status.HTTP_204_NO_CONTENT),
('/financial-assistance/course-v1:invalid+ErrorX+Invalid_Course/apply/', status.HTTP_400_BAD_REQUEST)
)
@ddt.unpack
def test_submit_financial_assistance_request_v2(self, referrer_url, expected_status, *args):
form_data = {
'username': self.user.username,
'course': 'course-v1:test+TestX+Test_Course',
@@ -979,9 +996,11 @@ class ViewsTestCase(BaseViewsTestCase):
'mktg-permission': False
}
response = self._submit_financial_assistance_form(
form_data, submit_url='submit_financial_assistance_request_v2'
form_data,
submit_url='submit_financial_assistance_request_v2',
referrer_url=referrer_url
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == expected_status
@ddt.data(
({}, 400),

View File

@@ -17,12 +17,14 @@ from xmodule.partitions.partitions_service import PartitionService # lint-amnes
from common.djangoapps.course_modes.models import CourseMode
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.courseware.config import ENABLE_NEW_FINANCIAL_ASSISTANCE_FLOW
from lms.djangoapps.courseware.constants import (
UNEXPECTED_ERROR_APPLICATION_STATUS,
UNEXPECTED_ERROR_CREATE_APPLICATION,
UNEXPECTED_ERROR_IS_ELIGIBLE
)
from lms.djangoapps.courseware.models import FinancialAssistanceConfiguration
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
log = logging.getLogger(__name__)
@@ -210,3 +212,19 @@ def get_course_hash_value(course_key):
return int(m.hexdigest(), base=16) % 100
return out_of_bound_value
def _use_new_financial_assistance_flow(course_id):
"""
Returns if the course_id can be used in the new financial assistance flow.
"""
is_financial_assistance_enabled_for_course = WaffleFlagCourseOverrideModel.override_value(
ENABLE_NEW_FINANCIAL_ASSISTANCE_FLOW.name, course_id
)
financial_assistance_configuration = FinancialAssistanceConfiguration.current()
if financial_assistance_configuration.enabled and (
is_financial_assistance_enabled_for_course == WaffleFlagCourseOverrideModel.ALL_CHOICES.on or
get_course_hash_value(course_id) <= financial_assistance_configuration.fa_backend_enabled_courses_percentage
):
return True
return False

View File

@@ -68,7 +68,6 @@ from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_home_api.toggles import course_home_mfe_progress_tab_is_active
from lms.djangoapps.courseware.access import has_access, has_ccx_coach_role
from lms.djangoapps.courseware.access_utils import check_course_open_for_learner, check_public_access
from lms.djangoapps.courseware.config import ENABLE_NEW_FINANCIAL_ASSISTANCE_FLOW
from lms.djangoapps.courseware.courses import (
can_self_enroll_in_course,
course_open_for_self_enrollment,
@@ -90,7 +89,11 @@ from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentMo
from lms.djangoapps.courseware.permissions import MASQUERADE_AS_STUDENT, VIEW_COURSE_HOME, VIEW_COURSEWARE
from lms.djangoapps.courseware.toggles import course_is_invitation_only
from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.courseware.utils import create_financial_assistance_application
from lms.djangoapps.courseware.utils import (
_use_new_financial_assistance_flow,
create_financial_assistance_application,
is_eligible_for_financial_aid
)
from lms.djangoapps.edxnotes.helpers import is_feature_enabled
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
from lms.djangoapps.grades.api import CourseGradeFactory
@@ -1902,10 +1905,18 @@ FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 1250 and 2500 characters or so in
@login_required
def financial_assistance(_request):
def financial_assistance(request, course_id=None):
"""Render the initial financial assistance page."""
reason = None
apply_url = reverse('financial_assistance_form')
if course_id and _use_new_financial_assistance_flow(course_id):
_, reason = is_eligible_for_financial_aid(course_id)
apply_url = reverse('financial_assistance_form_v2', args=[course_id])
return render_to_response('financial-assistance/financial-assistance.html', {
'header_text': _get_fa_header(FINANCIAL_ASSISTANCE_HEADER)
'header_text': _get_fa_header(FINANCIAL_ASSISTANCE_HEADER),
'apply_url': apply_url,
'reason': reason
})
@@ -1992,8 +2003,10 @@ def financial_assistance_request_v2(request):
if request.user.username != username:
return HttpResponseForbidden()
lms_user_id = request.user.id
course_id = data['course']
if course_id and course_id not in request.META.get('HTTP_REFERER'):
return HttpResponseBadRequest('Invalid Course ID provided.')
lms_user_id = request.user.id
income = data['income']
learner_reasons = data['reason_for_applying']
learner_goals = data['goals']
@@ -2020,10 +2033,13 @@ def financial_assistance_request_v2(request):
@login_required
def financial_assistance_form(request):
def financial_assistance_form(request, course_id=None):
"""Render the financial assistance application form page."""
user = request.user
enrolled_courses = get_financial_aid_courses(user)
disabled = False
if course_id:
disabled = True
enrolled_courses = get_financial_aid_courses(user, course_id)
incomes = ['Less than $5,000', '$5,000 - $10,000', '$10,000 - $15,000', '$15,000 - $20,000', '$20,000 - $25,000',
'$25,000 - $40,000', '$40,000 - $55,000', '$55,000 - $70,000', '$70,000 - $85,000',
'$85,000 - $100,000', 'More than $100,000']
@@ -2031,7 +2047,7 @@ def financial_assistance_form(request):
annual_incomes = [
{'name': _(income), 'value': income} for income in incomes # lint-amnesty, pylint: disable=translation-of-non-string
]
if ENABLE_NEW_FINANCIAL_ASSISTANCE_FLOW.is_enabled():
if course_id and _use_new_financial_assistance_flow(course_id):
submit_url = 'submit_financial_assistance_request_v2'
else:
submit_url = 'submit_financial_assistance_request'
@@ -2057,6 +2073,7 @@ def financial_assistance_form(request):
'placeholder': '',
'defaultValue': '',
'required': True,
'disabled': disabled,
'options': enrolled_courses,
'instructions': gettext(
'Select the course for which you want to earn a verified certificate. If'
@@ -2130,8 +2147,9 @@ def financial_assistance_form(request):
})
def get_financial_aid_courses(user):
def get_financial_aid_courses(user, course_id=None):
""" Retrieve the courses eligible for financial assistance. """
use_new_flow = False
financial_aid_courses = []
for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created'):
@@ -2142,6 +2160,15 @@ def get_financial_aid_courses(user):
Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC)),
course_id=enrollment.course_id,
mode_slug=CourseMode.VERIFIED).exists():
# This is a workaround to set course_id before disabling the field in case of new financial assistance flow.
if str(enrollment.course_overview) == course_id:
financial_aid_courses = [{
'name': enrollment.course_overview.display_name,
'value': str(enrollment.course_id),
'default': True
}]
use_new_flow = True
break
financial_aid_courses.append(
{
@@ -2150,6 +2177,9 @@ def get_financial_aid_courses(user):
}
)
if course_id is not None and use_new_flow is False:
# We don't want to show financial_aid_courses if the course_id is not found in the enrolled courses.
return []
return financial_aid_courses

View File

@@ -5,47 +5,54 @@ from django.urls import reverse
from django.utils.translation import ugettext as _
from common.djangoapps.edxmako.shortcuts import marketing_link
%>
<div class="financial-assistance-wrapper">
<div class="financial-assistance financial-assistance-header">
<h1>${_("Financial Assistance Application")}</h1>
% for line in header_text:
<p>${line}</p>
% endfor
</div>
% if reason:
<p>This course is not eligible for Financial Assistance for the following reason: <b>${reason}</b></p>
% else:
<div class="financial-assistance financial-assistance-header">
<h1>${_("Financial Assistance Application")}</h1>
% for line in header_text:
<p>${line}</p>
% endfor
</div>
<div class="financial-assistance financial-assistance-body">
<h2>${_("A Note to Learners")}</h2>
## Translators: This string will not be used in Open edX installations.
<p>${_("Dear edX Learner,")}</p>
<div class="financial-assistance financial-assistance-body">
<h2>${_("A Note to Learners")}</h2>
## Translators: This string will not be used in Open edX installations.
<p>${_("Dear edX Learner,")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("EdX Financial Assistance is a program we created to give learners in all financial circumstances a chance to earn a Verified Certificate upon successful completion of an edX course.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("EdX Financial Assistance is a program we created to give learners in all financial circumstances a chance to earn a Verified Certificate upon successful completion of an edX course.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note that financial assistance is limited and may not be awarded to all eligible candidates.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note that financial assistance is limited and may not be awarded to all eligible candidates.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("In order to be eligible for edX Financial Assistance, you must demonstrate that paying the Verified Certificate fee would cause you economic hardship. To apply, you will be asked to answer a few questions about why you are applying and how the Verified Certificate will benefit you.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("In order to be eligible for edX Financial Assistance, you must demonstrate that paying the Verified Certificate fee would cause you economic hardship. To apply, you will be asked to answer a few questions about why you are applying and how the Verified Certificate will benefit you.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("If your application is approved, we'll give you instructions for verifying your identity on edx.org so you can start working toward completing your edX course.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("If your application is approved, we'll give you instructions for verifying your identity on edx.org so you can start working toward completing your edX course.")}</p>
## Translators: This string will not be used in Open edX installations.
<p>${_("EdX is committed to making it possible for you to take high quality courses from leading institutions regardless of your financial situation, earn a Verified Certificate, and share your success with others.")}</p>
## Translators: This string will not be used in Open edX installations. Do not translate the name "Anant".
<p class="signature">${_("Sincerely, Anant")}</p>
</div>
% endif
## Translators: This string will not be used in Open edX installations.
<p>${_("EdX is committed to making it possible for you to take high quality courses from leading institutions regardless of your financial situation, earn a Verified Certificate, and share your success with others.")}</p>
## Translators: This string will not be used in Open edX installations. Do not translate the name "Anant".
<p class="signature">${_("Sincerely, Anant")}</p>
</div>
<div class="financial-assistance-footer">
<%
faq_link = marketing_link('FAQ')
%>
% if faq_link != '#':
<a class="faq-link" href="${faq_link}">${_("Back to Student FAQs")}</a>
% endif
<a class="action-link" href="${reverse('financial_assistance_form')}">${_("Apply for Financial Assistance")}</a>
<%
faq_link = marketing_link('FAQ')
%>
% if faq_link != '#':
<a class="faq-link" href="${faq_link}">${_("Back to Student FAQs")}</a>
% endif
% if not reason:
<a class="action-link" href="${apply_url}">${_("Apply for Financial Assistance")}</a>
% endif
</div>
</div>

View File

@@ -33,6 +33,8 @@
<% });
} %>
<% if ( required ) { %> aria-required="true" required<% } %>
<% if ( typeof disabled !== 'undefined' && disabled ) { %> disabled<% } %>
>
<% _.each(options, function(el) { %>
<option value="<%- el.value%>"<% if ( el.default ) { %> data-isdefault="true" selected<% } %>><%- el.name %></option>

View File

@@ -4,18 +4,18 @@ URLs for LMS
from config_models.views import ConfigurationModelCurrentAPIView
from django.conf import settings
from django.urls import include, re_path
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.admin import autodiscover as django_autodiscover
from django.urls import path
from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import RedirectView
from edx_api_doc_tools import make_docs_urls
from edx_django_utils.plugins import get_plugin_url_patterns
from django.contrib import admin
from common.djangoapps.student import views as student_views
from common.djangoapps.util import views as util_views
from lms.djangoapps.branding import views as branding_views
from lms.djangoapps.debug import views as debug_views
from lms.djangoapps.courseware.masquerade import MasqueradeView
from lms.djangoapps.courseware.module_render import (
handle_xblock_callback,
@@ -26,13 +26,14 @@ from lms.djangoapps.courseware.module_render import (
from lms.djangoapps.courseware.views import views as courseware_views
from lms.djangoapps.courseware.views.index import CoursewareIndex
from lms.djangoapps.courseware.views.views import CourseTabView, EnrollStaffView, StaticCourseTabView
from lms.djangoapps.debug import views as debug_views
from lms.djangoapps.discussion import views as discussion_views
from lms.djangoapps.discussion.config.settings import is_forum_daily_digest_enabled
from lms.djangoapps.discussion.notification_prefs import views as notification_prefs_views
from lms.djangoapps.instructor.views import instructor_dashboard as instructor_dashboard_views
from lms.djangoapps.instructor_task import views as instructor_task_views
from lms.djangoapps.staticbook import views as staticbook_views
from lms.djangoapps.static_template_view import views as static_template_view_views
from lms.djangoapps.staticbook import views as staticbook_views
from openedx.core.apidocs import api_info
from openedx.core.djangoapps.auth_exchange.views import LoginWithAccessTokenView
from openedx.core.djangoapps.catalog.models import CatalogIntegration
@@ -51,8 +52,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
from openedx.core.djangoapps.user_authn.views.login import redirect_to_lms_login
from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views
from openedx.features.enterprise_support.api import enterprise_enabled
from common.djangoapps.student import views as student_views
from common.djangoapps.util import views as util_views
RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines'
RENDER_XBLOCK_NAME = 'render_xblock'
@@ -240,9 +239,10 @@ urlpatterns += [
# Multicourse wiki (Note: wiki urls must be above the courseware ones because of
# the custom tab catch-all)
if settings.WIKI_ENABLED:
from wiki.urls import get_pattern as wiki_pattern
from lms.djangoapps.course_wiki import views as course_wiki_views
from django_notify.urls import get_pattern as notify_pattern
from wiki.urls import get_pattern as wiki_pattern
from lms.djangoapps.course_wiki import views as course_wiki_views
wiki_url_patterns, wiki_app_name = wiki_pattern()
notify_url_patterns, notify_app_name = notify_pattern()
@@ -948,6 +948,16 @@ if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'):
'financial-assistance_v2/submit/',
courseware_views.financial_assistance_request_v2,
name='submit_financial_assistance_request_v2'
),
re_path(
fr'financial-assistance/{settings.COURSE_ID_PATTERN}/apply/',
courseware_views.financial_assistance_form,
name='financial_assistance_form_v2'
),
re_path(
fr'financial-assistance/{settings.COURSE_ID_PATTERN}',
courseware_views.financial_assistance,
name='financial_assistance_v2'
)
]