Merge pull request #10858 from edx/bderusha/hcfa-language
Update copy for HCFA & don't show audit certs
This commit is contained in:
@@ -295,6 +295,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
CertificateStatuses.downloadable: 'ready',
|
||||
CertificateStatuses.notpassing: 'notpassing',
|
||||
CertificateStatuses.restricted: 'restricted',
|
||||
CertificateStatuses.auditing: 'auditing',
|
||||
}
|
||||
|
||||
default_status = 'processing'
|
||||
@@ -309,7 +310,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
if cert_status is None:
|
||||
return default_info
|
||||
|
||||
is_hidden_status = cert_status['status'] in ('unavailable', 'processing', 'generating', 'notpassing')
|
||||
is_hidden_status = cert_status['status'] in ('unavailable', 'processing', 'generating', 'notpassing', 'auditing')
|
||||
|
||||
if course_overview.certificates_display_behavior == 'early_no_info' and is_hidden_status:
|
||||
return {}
|
||||
@@ -325,7 +326,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES,
|
||||
}
|
||||
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing') and
|
||||
course_overview.end_of_course_survey_url is not None):
|
||||
status_dict.update({
|
||||
'show_survey_button': True,
|
||||
@@ -373,7 +374,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
cert_status['download_url']
|
||||
)
|
||||
|
||||
if status in ('generating', 'ready', 'notpassing', 'restricted'):
|
||||
if status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'):
|
||||
if 'grade' not in cert_status:
|
||||
# Note: as of 11/20/2012, we know there are students in this state-- cs169.1x,
|
||||
# who need to be regraded (we weren't tracking 'notpassing' at first).
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from functools import wraps
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.core.validators import ValidationError, validate_email
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
from django.views.defaults import server_error
|
||||
@@ -115,6 +116,10 @@ def calculate(request):
|
||||
|
||||
|
||||
class _ZendeskApi(object):
|
||||
|
||||
CACHE_PREFIX = 'ZENDESK_API_CACHE'
|
||||
CACHE_TIMEOUT = 60 * 60
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Instantiate the Zendesk API.
|
||||
@@ -150,8 +155,39 @@ class _ZendeskApi(object):
|
||||
"""
|
||||
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
|
||||
|
||||
def get_group(self, name):
|
||||
"""
|
||||
Find the Zendesk group named `name`. Groups are cached for
|
||||
CACHE_TIMEOUT seconds.
|
||||
|
||||
def _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info):
|
||||
If a matching group exists, it is returned as a dictionary
|
||||
with the format specifed by the zendesk package.
|
||||
|
||||
Otherwise, returns None.
|
||||
"""
|
||||
cache = caches['default']
|
||||
cache_key = '{prefix}_group_{name}'.format(prefix=self.CACHE_PREFIX, name=name)
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
groups = self._zendesk_instance.list_groups()['groups']
|
||||
for group in groups:
|
||||
if group['name'] == name:
|
||||
cache.set(cache_key, group, self.CACHE_TIMEOUT)
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
def _record_feedback_in_zendesk(
|
||||
realname,
|
||||
email,
|
||||
subject,
|
||||
details,
|
||||
tags,
|
||||
additional_info,
|
||||
group_name=None,
|
||||
require_update=False
|
||||
):
|
||||
"""
|
||||
Create a new user-requested Zendesk ticket.
|
||||
|
||||
@@ -159,6 +195,12 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio
|
||||
additional information from the browser and server, such as HTTP headers
|
||||
and user state. Returns a boolean value indicating whether ticket creation
|
||||
was successful, regardless of whether the private comment update succeeded.
|
||||
|
||||
If `group_name` is provided, attaches the ticket to the matching Zendesk group.
|
||||
|
||||
If `require_update` is provided, returns False when the update does not
|
||||
succeed. This allows using the private comment to add necessary information
|
||||
which the user will not see in followup emails from support.
|
||||
"""
|
||||
zendesk_api = _ZendeskApi()
|
||||
|
||||
@@ -184,8 +226,18 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio
|
||||
"tags": zendesk_tags
|
||||
}
|
||||
}
|
||||
group = None
|
||||
if group_name is not None:
|
||||
group = zendesk_api.get_group(group_name)
|
||||
if group is not None:
|
||||
new_ticket['ticket']['group_id'] = group['id']
|
||||
try:
|
||||
ticket_id = zendesk_api.create_ticket(new_ticket)
|
||||
if group is None:
|
||||
# Support uses Zendesk groups to track tickets. In case we
|
||||
# haven't been able to correctly group this ticket, log its ID
|
||||
# so it can be found later.
|
||||
log.warning('Unable to find group named %s for Zendesk ticket with ID %s.', group_name, ticket_id)
|
||||
except zendesk.ZendeskError:
|
||||
log.exception("Error creating Zendesk ticket")
|
||||
return False
|
||||
@@ -196,10 +248,12 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio
|
||||
try:
|
||||
zendesk_api.update_ticket(ticket_id, ticket_update)
|
||||
except zendesk.ZendeskError:
|
||||
log.exception("Error updating Zendesk ticket")
|
||||
# The update is not strictly necessary, so do not indicate failure to the user
|
||||
pass
|
||||
|
||||
log.exception("Error updating Zendesk ticket with ID %s.", ticket_id)
|
||||
# The update is not strictly necessary, so do not indicate
|
||||
# failure to the user unless it has been requested with
|
||||
# `require_update`.
|
||||
if require_update:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -143,15 +143,15 @@ from django.core.urlresolvers import reverse
|
||||
<div class="register-choice register-choice-audit">
|
||||
<div class="wrapper-copy">
|
||||
<span class="deco-ribbon"></span>
|
||||
<h4 class="title">${_("Earn an Honor Certificate")}</h4>
|
||||
<h4 class="title">${_("Audit This Course")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Take this course for free and have complete access to all the course material, activities, tests, and forums. Please note that learners who earn a passing grade will earn a certificate in this course.")}</p>
|
||||
<p>${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
<input type="submit" name="honor_mode" value="${_('Pursue an Honor Certificate')}" />
|
||||
<input type="submit" name="honor_mode" value="${_('Audit This Course')}" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -163,9 +163,10 @@ from django.core.urlresolvers import reverse
|
||||
<div class="register-choice register-choice-audit">
|
||||
<div class="wrapper-copy">
|
||||
<span class="deco-ribbon"></span>
|
||||
<h4 class="title">${_("Audit This Course")}</h4>
|
||||
<h4 class="title">${_("Audit This Course (No Certificate)")}</h4>
|
||||
<div class="copy">
|
||||
<p>${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. Please note that this track does not offer a certificate for learners who earn a passing grade.")}</p>
|
||||
## Translators: b_start notes the beginning of a section of text bolded for emphasis, and b_end marks the end of the bolded text.
|
||||
<p>${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. {b_start}Please note that this track does not offer a certificate for learners who earn a passing grade.{b_end}".format(**b_tag_kwargs))}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ class CertificateStatuses(object):
|
||||
regenerating = 'regenerating'
|
||||
restricted = 'restricted'
|
||||
unavailable = 'unavailable'
|
||||
auditing = 'auditing'
|
||||
|
||||
|
||||
class CertificateSocialNetworks(object):
|
||||
@@ -305,10 +306,20 @@ def certificate_status_for_student(student, course_id):
|
||||
}
|
||||
if generated_certificate.grade:
|
||||
cert_status['grade'] = generated_certificate.grade
|
||||
|
||||
if generated_certificate.mode == 'audit':
|
||||
course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)]
|
||||
# Short term fix to make sure old audit users with certs still see their certs
|
||||
# only do this if there if no honor mode
|
||||
if 'honor' not in course_mode_slugs:
|
||||
cert_status['status'] = CertificateStatuses.auditing
|
||||
return cert_status
|
||||
|
||||
if generated_certificate.status == CertificateStatuses.downloadable:
|
||||
cert_status['download_url'] = generated_certificate.download_url
|
||||
|
||||
return cert_status
|
||||
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
pass
|
||||
return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor}
|
||||
|
||||
@@ -536,31 +536,32 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
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)
|
||||
mocked_kwargs = mock_record_feedback.call_args[1]
|
||||
group_name = mocked_kwargs['group_name']
|
||||
require_update = mocked_kwargs['require_update']
|
||||
private_comment = '\n'.join(additional_info.values())
|
||||
for info in (country, income, reason_for_applying, goals, effort, username, legal_name, course):
|
||||
self.assertIn(info, private_comment)
|
||||
|
||||
self.assertEqual(additional_info['Allowed for marketing purposes'], 'No')
|
||||
|
||||
self.assertEqual(
|
||||
ticket_subject,
|
||||
'Financial assistance request for user {username} in course {course}'.format(
|
||||
'Financial assistance request for learner {username} in course {course}'.format(
|
||||
username=username,
|
||||
course=course
|
||||
course=self.course.display_name
|
||||
)
|
||||
)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
'issue_type': 'Financial Assistance',
|
||||
'course_id': course
|
||||
},
|
||||
tags
|
||||
)
|
||||
self.assertDictContainsSubset({'course_id': course}, tags)
|
||||
self.assertIn('Client IP', additional_info)
|
||||
self.assertEqual(group_name, 'Financial Assistance')
|
||||
self.assertTrue(require_update)
|
||||
|
||||
@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': '',
|
||||
'course': unicode(self.course.id),
|
||||
'name': '',
|
||||
'email': '',
|
||||
'country': '',
|
||||
@@ -574,7 +575,8 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
|
||||
@ddt.data(
|
||||
({}, 400),
|
||||
({'username': 'wwhite'}, 403)
|
||||
({'username': 'wwhite'}, 403),
|
||||
({'username': 'dummy', 'course': 'bad course ID'}, 400)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_submit_financial_assistance_errors(self, data, status):
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import textwrap
|
||||
import urllib
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@@ -1415,20 +1416,22 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
|
||||
# Translators: "percent_sign" is the symbol "%". "platform_name" is a
|
||||
# string identifying the name of this installation, such as "edX".
|
||||
FINANCIAL_ASSISTANCE_HEADER = _(
|
||||
'{platform_name} now offers financial assistance for learners who want to earn verified certificates but'
|
||||
'{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but'
|
||||
' who may not be able to pay the Verified Certificate fee. Eligible learners receive 90{percent_sign} off'
|
||||
' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the'
|
||||
' audit track for a course that offers Verified Certificates, and then complete this application.'
|
||||
' Note that you must complete a separate application for each course you take.'
|
||||
' Note that you must complete a separate application for each course you take.\n We will use this'
|
||||
'information to evaluate your application for financial assistance and to further develop our'
|
||||
'financial assistance program.'
|
||||
).format(
|
||||
percent_sign="%",
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
).split('\n')
|
||||
|
||||
|
||||
FA_INCOME_LABEL = _('Annual Income')
|
||||
FA_INCOME_LABEL = _('Annual Household Income')
|
||||
FA_REASON_FOR_APPLYING_LABEL = _(
|
||||
'Tell us about your current financial situation, including any unusual circumstances.'
|
||||
'Tell us about your current financial situation.'
|
||||
)
|
||||
FA_GOALS_LABEL = _(
|
||||
'Tell us about your learning or professional goals. How will a Verified Certificate in'
|
||||
@@ -1436,7 +1439,7 @@ FA_GOALS_LABEL = _(
|
||||
)
|
||||
FA_EFFORT_LABEL = _(
|
||||
'Tell us about your plans for this course. What steps will you take to help you complete'
|
||||
' the course work a receive a certificate?'
|
||||
' the course work and receive a certificate?'
|
||||
)
|
||||
FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 250 and 500 words or so in your response.')
|
||||
|
||||
@@ -1462,6 +1465,7 @@ def financial_assistance_request(request):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
course_id = data['course']
|
||||
course = modulestore().get_course(CourseKey.from_string(course_id))
|
||||
legal_name = data['name']
|
||||
email = data['email']
|
||||
country = data['country']
|
||||
@@ -1474,52 +1478,40 @@ def financial_assistance_request(request):
|
||||
except ValueError:
|
||||
# Thrown if JSON parsing fails
|
||||
return HttpResponseBadRequest('Could not parse request JSON.')
|
||||
except InvalidKeyError:
|
||||
# Thrown if course key parsing fails
|
||||
return HttpResponseBadRequest('Could not parse request course key.')
|
||||
except KeyError as err:
|
||||
# Thrown if fields are missing
|
||||
return HttpResponseBadRequest('The field {} is required.'.format(err.message))
|
||||
|
||||
ticket_body = textwrap.dedent(
|
||||
'''
|
||||
Annual Income: {income}
|
||||
Country: {country}
|
||||
|
||||
{reason_label}
|
||||
{separator}
|
||||
{reason_for_applying}
|
||||
|
||||
{goals_label}
|
||||
{separator}
|
||||
{goals}
|
||||
|
||||
{effort_label}
|
||||
{separator}
|
||||
{effort}
|
||||
|
||||
This user {allowed_for_marketing} allowed this content to be used for edX marketing purposes.
|
||||
'''.format(
|
||||
income=income,
|
||||
country=country,
|
||||
reason_label=FA_REASON_FOR_APPLYING_LABEL,
|
||||
reason_for_applying=reason_for_applying,
|
||||
goals_label=FA_GOALS_LABEL,
|
||||
goals=goals,
|
||||
effort_label=FA_EFFORT_LABEL,
|
||||
effort=effort,
|
||||
allowed_for_marketing='HAS' if marketing_permission else 'HAS NOT',
|
||||
separator='=' * 16
|
||||
)
|
||||
)
|
||||
|
||||
zendesk_submitted = _record_feedback_in_zendesk(
|
||||
legal_name,
|
||||
email,
|
||||
'Financial assistance request for user {username} in course {course_id}'.format(
|
||||
'Financial assistance request for learner {username} in course {course_name}'.format(
|
||||
username=username,
|
||||
course_id=course_id
|
||||
course_name=course.display_name
|
||||
),
|
||||
ticket_body,
|
||||
{'issue_type': 'Financial Assistance', 'course_id': course_id},
|
||||
{'Client IP': ip_address}
|
||||
'Financial Assistance Request',
|
||||
{'course_id': course_id},
|
||||
# Send the application as additional info on the ticket so
|
||||
# that it is not shown when support replies. This uses
|
||||
# OrderedDict so that information is presented in the right
|
||||
# order.
|
||||
OrderedDict((
|
||||
('Username', username),
|
||||
('Full Name', legal_name),
|
||||
('Course ID', course_id),
|
||||
('Annual Household Income', income),
|
||||
('Country', country),
|
||||
('Allowed for marketing purposes', 'Yes' if marketing_permission else 'No'),
|
||||
(FA_REASON_FOR_APPLYING_LABEL, '\n' + reason_for_applying + '\n\n'),
|
||||
(FA_GOALS_LABEL, '\n' + goals + '\n\n'),
|
||||
(FA_EFFORT_LABEL, '\n' + effort + '\n\n'),
|
||||
('Client IP', ip_address),
|
||||
)),
|
||||
group_name='Financial Assistance',
|
||||
require_update=True
|
||||
)
|
||||
|
||||
if not zendesk_submitted:
|
||||
@@ -1630,7 +1622,8 @@ def financial_assistance_form(request):
|
||||
'type': 'checkbox',
|
||||
'required': False,
|
||||
'instructions': _(
|
||||
'Annual income and personal information such as email address will not be shared.'
|
||||
'Annual income and personal information such as email address will not be shared. '
|
||||
'Financial information will not be used for marketing purposes.'
|
||||
),
|
||||
'restrictions': {}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
|
||||
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
||||
def test_web_certificate(self):
|
||||
CourseMode.objects.create(
|
||||
course_id=self.course.id,
|
||||
mode_display_name="Honor",
|
||||
mode_slug=CourseMode.HONOR,
|
||||
)
|
||||
self.login_and_enroll()
|
||||
|
||||
self.course.cert_html_view_enabled = True
|
||||
|
||||
@@ -27,10 +27,9 @@ else:
|
||||
status_css_class = 'course-status-processing'
|
||||
%>
|
||||
<div class="message message-status ${status_css_class} is-shown">
|
||||
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}</p>
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
|
||||
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'):
|
||||
<p class="message-copy">${_("Your final grade:")}
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
|
||||
@@ -19,9 +19,9 @@ from edxmako.shortcuts import marketing_link
|
||||
<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>${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note financial assistance 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>${_("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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user