diff --git a/lms/djangoapps/courseware/constants.py b/lms/djangoapps/courseware/constants.py index dcde5f7024..1c7ea10097 100644 --- a/lms/djangoapps/courseware/constants.py +++ b/lms/djangoapps/courseware/constants.py @@ -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" diff --git a/lms/djangoapps/courseware/course_tools.py b/lms/djangoapps/courseware/course_tools.py index a60a6cf278..c0e37c76e0 100644 --- a/lms/djangoapps/courseware/course_tools.py +++ b/lms/djangoapps/courseware/course_tools.py @@ -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') diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index c9fa87a0e1..bd313558bb 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -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), diff --git a/lms/djangoapps/courseware/utils.py b/lms/djangoapps/courseware/utils.py index 9eafc2a775..6cd32dca2b 100644 --- a/lms/djangoapps/courseware/utils.py +++ b/lms/djangoapps/courseware/utils.py @@ -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 diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 71d75e3a53..a2c30fd5d5 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -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 diff --git a/lms/templates/financial-assistance/financial-assistance.html b/lms/templates/financial-assistance/financial-assistance.html index 9389af9185..a41799293f 100644 --- a/lms/templates/financial-assistance/financial-assistance.html +++ b/lms/templates/financial-assistance/financial-assistance.html @@ -5,47 +5,54 @@ from django.urls import reverse from django.utils.translation import ugettext as _ from common.djangoapps.edxmako.shortcuts import marketing_link + %>
${line}
- % endfor -This course is not eligible for Financial Assistance for the following reason: ${reason}
+ % else: +${line}
+ % endfor +${_("Dear edX Learner,")}
-${_("Dear edX Learner,")}
+ ## Translators: This string will not be used in Open edX installations. +${_("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.")}
- ## Translators: This string will not be used in Open edX installations. -${_("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.")}
+ ## Translators: This string will not be used in Open edX installations. +${_("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.")}
- ## Translators: This string will not be used in Open edX installations. -${_("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.")}
+ ## Translators: This string will not be used in Open edX installations. +${_("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.")}
- ## Translators: This string will not be used in Open edX installations. -${_("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.")}
+ ## Translators: This string will not be used in Open edX installations. +${_("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.")}
- ## Translators: This string will not be used in Open edX installations. -${_("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.")}
+ ## Translators: This string will not be used in Open edX installations. +${_("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.")}
+ ## Translators: This string will not be used in Open edX installations. Do not translate the name "Anant". +${_("Sincerely, Anant")}
+${_("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.")}
- ## Translators: This string will not be used in Open edX installations. Do not translate the name "Anant". -${_("Sincerely, Anant")}
-