Commit 66719b1b by Peter Fogg

Add financial assistance page.

Page skeleton and endpoint to submit FA requests to Zendesk.

ECOM-2824
ECOM-3038
parent 47b0d492
...@@ -8,7 +8,7 @@ import ddt ...@@ -8,7 +8,7 @@ import ddt
import json import json
import itertools import itertools
import unittest import unittest
from datetime import datetime from datetime import datetime, timedelta
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -32,6 +32,7 @@ from certificates import api as certs_api ...@@ -32,6 +32,7 @@ from certificates import api as certs_api
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
from certificates.tests.factories import GeneratedCertificateFactory from certificates.tests.factories import GeneratedCertificateFactory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.model_data import set_score from courseware.model_data import set_score
from courseware.testutils import RenderXBlockTestMixin from courseware.testutils import RenderXBlockTestMixin
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
...@@ -190,7 +191,8 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -190,7 +191,8 @@ class ViewsTestCase(ModuleStoreTestCase):
self.component = ItemFactory.create(category='problem', parent_location=self.vertical.location) self.component = ItemFactory.create(category='problem', parent_location=self.vertical.location)
self.course_key = self.course.id self.course_key = self.course.id
self.user = UserFactory(username='dummy', password='123456', email='test@mit.edu') self.password = '123456'
self.user = UserFactory(username='dummy', password=self.password, email='test@mit.edu')
self.date = datetime(2013, 1, 22, tzinfo=UTC) self.date = datetime(2013, 1, 22, tzinfo=UTC)
self.enrollment = CourseEnrollment.enroll(self.user, self.course_key) self.enrollment = CourseEnrollment.enroll(self.user, self.course_key)
self.enrollment.created = self.date self.enrollment.created = self.date
...@@ -270,7 +272,7 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -270,7 +272,7 @@ class ViewsTestCase(ModuleStoreTestCase):
self.section.location.name, self.section.location.name,
'f' 'f'
]) ])
self.client.login(username=self.user.username, password="123456") self.client.login(username=self.user.username, password=self.password)
response = self.client.get(request_url) response = self.client.get(request_url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -283,7 +285,7 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -283,7 +285,7 @@ class ViewsTestCase(ModuleStoreTestCase):
self.section.location.name, self.section.location.name,
'1' '1'
] ]
self.client.login(username=self.user.username, password="123456") self.client.login(username=self.user.username, password=self.password)
for idx, val in enumerate(url_parts): for idx, val in enumerate(url_parts):
url_parts_copy = url_parts[:] url_parts_copy = url_parts[:]
url_parts_copy[idx] = val + u'χ' url_parts_copy[idx] = val + u'χ'
...@@ -458,6 +460,136 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -458,6 +460,136 @@ class ViewsTestCase(ModuleStoreTestCase):
# Verify that the email opt-in checkbox does not appear # Verify that the email opt-in checkbox does not appear
self.assertNotContains(response, checkbox_html, html=True) self.assertNotContains(response, checkbox_html, html=True)
def test_financial_assistance_page(self):
self.client.login(username=self.user.username, password=self.password)
url = reverse('financial_assistance')
response = self.client.get(url)
# This is a static page, so just assert that it is returned correctly
self.assertEqual(response.status_code, 200)
self.assertIn('Financial Assistance Application', response.content)
def test_financial_assistance_form(self):
non_verified_course = CourseFactory.create().id
verified_course_verified_track = CourseFactory.create().id
verified_course_audit_track = CourseFactory.create().id
verified_course_deadline_passed = CourseFactory.create().id
unenrolled_course = CourseFactory.create().id
enrollments = (
(non_verified_course, CourseMode.AUDIT, None),
(verified_course_verified_track, CourseMode.VERIFIED, None),
(verified_course_audit_track, CourseMode.AUDIT, None),
(verified_course_deadline_passed, CourseMode.AUDIT, datetime.now(UTC) - timedelta(days=1))
)
for course, mode, expiration in enrollments:
CourseModeFactory(mode_slug=CourseMode.AUDIT, course_id=course)
if course != non_verified_course:
CourseModeFactory(mode_slug=CourseMode.VERIFIED, course_id=course, expiration_datetime=expiration)
CourseEnrollmentFactory(course_id=course, user=self.user, mode=mode)
self.client.login(username=self.user.username, password=self.password)
url = reverse('financial_assistance_form')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Ensure that the user can only apply for assistance in
# courses which have a verified mode which hasn't expired yet,
# where the user is not already enrolled in verified mode
self.assertIn(str(verified_course_audit_track), response.content)
for course in (
non_verified_course,
verified_course_verified_track,
verified_course_deadline_passed,
unenrolled_course
):
self.assertNotIn(str(course), response.content)
def _submit_financial_assistance_form(self, data):
"""Submit a financial assistance request."""
self.client.login(username=self.user.username, password=self.password)
url = reverse('submit_financial_assistance_request')
return self.client.post(url, json.dumps(data), content_type='application/json')
@patch.object(views, '_record_feedback_in_zendesk')
def test_submit_financial_assistance_request(self, mock_record_feedback):
username = self.user.username
course = unicode(self.course_key)
legal_name = 'Jesse Pinkman'
country = 'United States'
income = '1234567890'
reason_for_applying = "It's just basic chemistry, yo."
goals = "I don't know if it even matters, but... work with my hands, I guess."
effort = "I'm done, okay? You just give me my money, and you and I, we're done."
data = {
'username': username,
'course_id': course,
'legal_name': legal_name,
'email': self.user.email,
'country': country,
'income': income,
'reason_for_applying': reason_for_applying,
'goals': goals,
'effort': effort,
'marketing_permission': False,
}
response = self._submit_financial_assistance_form(data)
self.assertEqual(response.status_code, 204)
__, ___, ticket_subject, ticket_body, tags, additional_info = mock_record_feedback.call_args[0]
for info in (country, income, reason_for_applying, goals, effort):
self.assertIn(info, ticket_body)
self.assertIn('This user HAS NOT allowed this content to be used for edX marketing purposes.', ticket_body)
self.assertEqual(
ticket_subject,
'Financial assistance request for user {username} in course {course}'.format(
username=username,
course=course
)
)
self.assertDictContainsSubset(
{
'issue_type': 'Financial Assistance',
'course_id': course
},
tags
)
self.assertIn('Client IP', additional_info)
@patch.object(views, '_record_feedback_in_zendesk', return_value=False)
def test_zendesk_submission_failed(self, _mock_record_feedback):
response = self._submit_financial_assistance_form({
'username': self.user.username,
'course_id': '',
'legal_name': '',
'email': '',
'country': '',
'income': '',
'reason_for_applying': '',
'goals': '',
'effort': '',
'marketing_permission': False,
})
self.assertEqual(response.status_code, 500)
@ddt.data(
({}, 400),
({'username': 'wwhite'}, 403)
)
@ddt.unpack
def test_submit_financial_assistance_errors(self, data, status):
response = self._submit_financial_assistance_form(data)
self.assertEqual(response.status_code, status)
def test_financial_assistance_login_required(self):
for url in (
reverse('financial_assistance'),
reverse('financial_assistance_form'),
reverse('submit_financial_assistance_request')
):
response = self.client.get(url)
self.assertRedirects(response, reverse('signin_user') + '?next=' + url)
@attr('shard_1') @attr('shard_1')
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly # setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
......
...@@ -2732,3 +2732,10 @@ PROCTORING_SETTINGS = {} ...@@ -2732,3 +2732,10 @@ PROCTORING_SETTINGS = {}
# The reason we introcuced this number is because we do not want the CCX # The reason we introcuced this number is because we do not want the CCX
# to compete with the MOOC. # to compete with the MOOC.
CCX_MAX_STUDENTS_ALLOWED = 200 CCX_MAX_STUDENTS_ALLOWED = 200
# Financial assistance settings
# Maximum and minimum length of answers, in characters, for the
# financial assistance form
FINANCIAL_ASSISTANCE_MIN_LENGTH = 800
FINANCIAL_ASSISTANCE_MAX_LENGTH = 2500
...@@ -549,3 +549,6 @@ AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend',) ...@@ -549,3 +549,6 @@ AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend',)
# ORGANIZATIONS # ORGANIZATIONS
FEATURES['ORGANIZATIONS_APP'] = True FEATURES['ORGANIZATIONS_APP'] = True
# Financial assistance page
FEATURES['ENABLE_FINANCIAL_ASSISTANCE_FORM'] = True
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
@import 'views/shoppingcart'; @import 'views/shoppingcart';
@import 'views/homepage'; @import 'views/homepage';
@import 'views/support'; @import 'views/support';
@import "views/financial-assistance";
@import 'course/auto-cert'; @import 'course/auto-cert';
// app - discussion // app - discussion
......
.financial-assistance-wrapper {
margin: auto;
padding: $baseline 0;
max-width: 1180px;
.financial-assistance {
border-bottom: 4px solid $gray-l5;
h1 {
@extend %t-title4;
@include text-align(left);
margin: 0;
padding: ($baseline/2) 0;
border-bottom: 4px solid $gray-l5;
color: $m-gray-d3;
}
h2 {
@extend %t-title6;
@extend %t-strong;
margin-top: ($baseline/2);
text-transform: none;
}
p {
@extend %t-copy-base;
padding: ($baseline/2) 0;
margin: 0;
color: $m-gray-d2;
}
.apply-form-list {
padding: 0;
list-style: none;
.apply-form-section {
border-bottom: 2px solid $gray-l5;
}
.apply-form-section:last-child {
border: none;
}
.about-me {
padding: 0;
list-style: none;
.about-me-item {
@include margin-right(150px);
display: inline-block;
p {
padding: 0;
display: block;
}
}
}
}
}
.financial-assistance-footer {
padding: $baseline;
.faq-link {
padding: $baseline/2;
}
.action-link {
@include float(right);
padding: $baseline/2;
background-color: $m-blue-d2;
color: $gray-l7;
border-radius: 2px;
}
}
}
<%inherit file="../main.html"/>
<%!
import json
from openedx.core.lib.js_utils import escape_json_dumps
%>
<%namespace name='static' file='/static_content.html'/>
<%block name="js_extra">
<%static:require_module module_name="js/financial-assistance/financial_assistance_form_factory" class_name="FinancialAssistanceFactory">
FinancialAssistanceFactory({
fields: ${escape_json_dumps(fields)},
user_details: ${escape_json_dumps(user_details)},
header_text: ${escape_json_dumps(header_text)},
student_faq_url: ${json.dumps(student_faq_url)},
dashboard_url: ${json.dumps(dashboard_url)},
platform_name: ${escape_json_dumps(platform_name)},
submit_url: ${json.dumps(submit_url)}
});
</%static:require_module>
</%block>
<div class="financial-assistance-wrapper"></div>
<%inherit file="../main.html"/>
<%
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from 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>
<div class="financial-assistance financial-assistance-body">
<h2>${_("A Note to Learners")}</h2>
<p>${_("Dear edX Learner,")}</p>
<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>
<p>${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note space is limited.")}</p>
<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>
<p>${_("Once your application is approved, we'll email to let you know and give you instructions for how to verify your identity on edX.org; then you can start working toward completing your edX course.")}</p>
<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>
<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>
</div>
</div>
...@@ -774,3 +774,22 @@ urlpatterns += ( ...@@ -774,3 +774,22 @@ urlpatterns += (
urlpatterns += ( urlpatterns += (
url(r'^api/', include('edx_proctoring.urls')), url(r'^api/', include('edx_proctoring.urls')),
) )
if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'):
urlpatterns += (
url(
r'^financial-assistance/$',
'courseware.views.financial_assistance',
name='financial_assistance'
),
url(
r'^financial-assistance/apply/$',
'courseware.views.financial_assistance_form',
name='financial_assistance_form'
),
url(
r'^financial-assistance/submit/$',
'courseware.views.financial_assistance_request',
name='submit_financial_assistance_request'
)
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment