@@ -39,7 +39,9 @@ class CourseModeForm(forms.ModelForm):
|
||||
[(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] +
|
||||
[(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] +
|
||||
[(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)] +
|
||||
[(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES]
|
||||
[(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES] +
|
||||
# need to keep legacy modes around for awhile
|
||||
[(CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)]
|
||||
)
|
||||
|
||||
mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES, label=_("Mode"))
|
||||
|
||||
@@ -114,6 +114,13 @@ class CourseMode(models.Model):
|
||||
# Modes that are allowed to upsell
|
||||
UPSELL_TO_VERIFIED_MODES = [HONOR, AUDIT]
|
||||
|
||||
# Courses purchased through the shoppingcart
|
||||
# should be "honor". Since we've changed the DEFAULT_MODE_SLUG from
|
||||
# "honor" to "audit", we still need to have the shoppingcart
|
||||
# use "honor"
|
||||
DEFAULT_SHOPPINGCART_MODE_SLUG = HONOR
|
||||
DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', 'usd', None, None, None)
|
||||
|
||||
class Meta(object):
|
||||
unique_together = ('course_id', 'mode_slug', 'currency')
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -369,7 +370,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):
|
||||
@@ -306,10 +307,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, 'uuid': None}
|
||||
|
||||
@@ -522,59 +522,61 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
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,
|
||||
'course': course,
|
||||
'name': legal_name,
|
||||
'email': self.user.email,
|
||||
'country': country,
|
||||
'income': income,
|
||||
'reason_for_applying': reason_for_applying,
|
||||
'goals': goals,
|
||||
'effort': effort,
|
||||
'marketing_permission': False,
|
||||
'mktg-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)
|
||||
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_id': '',
|
||||
'legal_name': '',
|
||||
'course': unicode(self.course.id),
|
||||
'name': '',
|
||||
'email': '',
|
||||
'country': '',
|
||||
'income': '',
|
||||
'reason_for_applying': '',
|
||||
'goals': '',
|
||||
'effort': '',
|
||||
'marketing_permission': False,
|
||||
'mktg-permission': False,
|
||||
})
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
@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 _
|
||||
|
||||
@@ -1404,6 +1405,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
|
||||
'disable_accordion': True,
|
||||
'allow_iframing': True,
|
||||
'disable_header': True,
|
||||
'disable_footer': True,
|
||||
'disable_window_wrap': True,
|
||||
'disable_preview_menu': True,
|
||||
'staff_access': bool(has_access(request.user, 'staff', course)),
|
||||
@@ -1415,20 +1417,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 +1440,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.')
|
||||
|
||||
@@ -1461,65 +1465,54 @@ def financial_assistance_request(request):
|
||||
if request.user.username != username:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
course_id = data['course_id']
|
||||
legal_name = data['legal_name']
|
||||
course_id = data['course']
|
||||
course = modulestore().get_course(CourseKey.from_string(course_id))
|
||||
legal_name = data['name']
|
||||
email = data['email']
|
||||
country = data['country']
|
||||
income = data['income']
|
||||
reason_for_applying = data['reason_for_applying']
|
||||
goals = data['goals']
|
||||
effort = data['effort']
|
||||
marketing_permission = data['marketing_permission']
|
||||
marketing_permission = data['mktg-permission']
|
||||
ip_address = get_ip(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 +1623,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': {}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,16 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
|
||||
if previous_state.user:
|
||||
# if the student is currently unenrolled, don't enroll them in their
|
||||
# previous mode
|
||||
course_mode = CourseMode.DEFAULT_MODE_SLUG
|
||||
|
||||
# for now, White Labels use 'shoppingcart' which is based on the
|
||||
# "honor" course_mode. Given the change to use "audit" as the default
|
||||
# course_mode in Open edX, we need to be backwards compatible with
|
||||
# how White Labels approach enrollment modes.
|
||||
if CourseMode.is_white_label(course_id):
|
||||
course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
|
||||
else:
|
||||
course_mode = CourseMode.DEFAULT_MODE_SLUG
|
||||
|
||||
if previous_state.enrollment:
|
||||
course_mode = previous_state.mode
|
||||
|
||||
|
||||
@@ -362,6 +362,11 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour
|
||||
def setUp(self):
|
||||
super(TestInstructorDetailedEnrollmentReport, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
min_price=50,
|
||||
mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
|
||||
)
|
||||
|
||||
# create testing invoice 1
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
@@ -476,7 +481,7 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour
|
||||
created_by=self.instructor,
|
||||
invoice=self.sale_invoice_1,
|
||||
invoice_item=self.invoice_item,
|
||||
mode_slug=CourseMode.DEFAULT_MODE_SLUG
|
||||
mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
@@ -517,7 +522,7 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour
|
||||
created_by=self.instructor,
|
||||
invoice=self.sale_invoice_1,
|
||||
invoice_item=self.invoice_item,
|
||||
mode_slug=CourseMode.DEFAULT_MODE_SLUG
|
||||
mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
@@ -845,7 +850,11 @@ class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase):
|
||||
def setUp(self):
|
||||
super(TestExecutiveSummaryReport, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course.id, min_price=50)
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
min_price=50,
|
||||
mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
|
||||
)
|
||||
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.student1 = UserFactory()
|
||||
|
||||
@@ -229,6 +229,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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=arguments-differ
|
||||
""" Models for the shopping cart and assorted purchase types """
|
||||
|
||||
from collections import namedtuple
|
||||
@@ -1473,7 +1474,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
app_label = "shoppingcart"
|
||||
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)
|
||||
course_enrollment = models.ForeignKey(CourseEnrollment, null=True)
|
||||
|
||||
@classmethod
|
||||
@@ -1526,7 +1527,8 @@ class PaidCourseRegistration(OrderItem):
|
||||
|
||||
@classmethod
|
||||
@transaction.atomic
|
||||
def add_to_order(cls, order, course_id, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None):
|
||||
def add_to_order(cls, order, course_id, mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG,
|
||||
cost=None, currency=None): # pylint: disable=arguments-differ
|
||||
"""
|
||||
A standardized way to create these objects, with sensible defaults filled in.
|
||||
Will update the cost if called on an order that already carries the course.
|
||||
@@ -1561,7 +1563,7 @@ class PaidCourseRegistration(OrderItem):
|
||||
course_mode = CourseMode.mode_for_course(course_id, mode_slug)
|
||||
if not course_mode:
|
||||
# user could have specified a mode that's not set, in that case return the DEFAULT_MODE
|
||||
course_mode = CourseMode.DEFAULT_MODE
|
||||
course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE
|
||||
if not cost:
|
||||
cost = course_mode.min_price
|
||||
if not currency:
|
||||
@@ -1660,7 +1662,7 @@ class CourseRegCodeItem(OrderItem):
|
||||
app_label = "shoppingcart"
|
||||
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)
|
||||
|
||||
@classmethod
|
||||
def get_bulk_purchased_seat_count(cls, course_key, status='purchased'):
|
||||
@@ -1706,7 +1708,8 @@ class CourseRegCodeItem(OrderItem):
|
||||
|
||||
@classmethod
|
||||
@transaction.atomic
|
||||
def add_to_order(cls, order, course_id, qty, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None): # pylint: disable=arguments-differ
|
||||
def add_to_order(cls, order, course_id, qty, mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG,
|
||||
cost=None, currency=None): # pylint: disable=arguments-differ
|
||||
"""
|
||||
A standardized way to create these objects, with sensible defaults filled in.
|
||||
Will update the cost if called on an order that already carries the course.
|
||||
@@ -1736,8 +1739,8 @@ class CourseRegCodeItem(OrderItem):
|
||||
### handle default arguments for mode_slug, cost, currency
|
||||
course_mode = CourseMode.mode_for_course(course_id, mode_slug)
|
||||
if not course_mode:
|
||||
# user could have specified a mode that's not set, in that case return the DEFAULT_MODE
|
||||
course_mode = CourseMode.DEFAULT_MODE
|
||||
# user could have specified a mode that's not set, in that case return the DEFAULT_SHOPPINGCART_MODE
|
||||
course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE
|
||||
if not cost:
|
||||
cost = course_mode.min_price
|
||||
if not currency:
|
||||
|
||||
@@ -732,7 +732,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(reg1.unit_cost, 0)
|
||||
self.assertEqual(reg1.line_cost, 0)
|
||||
self.assertEqual(reg1.mode, CourseMode.DEFAULT_MODE_SLUG)
|
||||
self.assertEqual(reg1.mode, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)
|
||||
self.assertEqual(reg1.user, self.user)
|
||||
self.assertEqual(reg1.status, "cart")
|
||||
self.assertEqual(self.cart.total_cost, 0)
|
||||
@@ -742,7 +742,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(course_reg_code_item.unit_cost, 0)
|
||||
self.assertEqual(course_reg_code_item.line_cost, 0)
|
||||
self.assertEqual(course_reg_code_item.mode, CourseMode.DEFAULT_MODE_SLUG)
|
||||
self.assertEqual(course_reg_code_item.mode, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)
|
||||
self.assertEqual(course_reg_code_item.user, self.user)
|
||||
self.assertEqual(course_reg_code_item.status, "cart")
|
||||
self.assertEqual(self.cart.total_cost, 0)
|
||||
|
||||
@@ -247,13 +247,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin):
|
||||
test to check that that the same coupon code applied on multiple
|
||||
items in the cart.
|
||||
"""
|
||||
for course_key, cost in ((self.course_key, 40), (self.testing_course.id, 20)):
|
||||
CourseMode(
|
||||
course_id=course_key,
|
||||
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
|
||||
mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
|
||||
min_price=cost
|
||||
).save()
|
||||
|
||||
self.login_user()
|
||||
# add first course to user cart
|
||||
resp = self.client.post(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'js/financial-assistance/views/financial_assistance_form_view'
|
||||
],
|
||||
function (FinancialAssistanceFormView) {
|
||||
return function (options) {
|
||||
var formView = new FinancialAssistanceFormView({
|
||||
el: '.financial-assistance-wrapper',
|
||||
context: options
|
||||
});
|
||||
|
||||
return formView;
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Model for Financial Assistance.
|
||||
*/
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define(['backbone'], function (Backbone) {
|
||||
var FinancialAssistance = Backbone.Model.extend({
|
||||
initialize: function(options) {
|
||||
this.url = options.url;
|
||||
}
|
||||
});
|
||||
return FinancialAssistance;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,48 @@
|
||||
<h1><%- gettext('Financial Assistance Application') %></h1>
|
||||
|
||||
<div class="intro">
|
||||
<% _.each(header_text, function(copy) { %>
|
||||
<p class="copy"><%- copy %></p>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<form class="financial-assistance-form">
|
||||
<div class="status submission-error hidden" aria-live="polite">
|
||||
<h4 class="message-title"><%- gettext('Application not submitted') %></h4>
|
||||
<ul class="message-copy"></ul>
|
||||
</div>
|
||||
|
||||
<div class="user-info">
|
||||
<h2><%- gettext('About You') %></h2>
|
||||
<p><%- interpolate_text(
|
||||
gettext('The following information is already a part of your {platform} profile. We\'ve included it here for your application.'),
|
||||
{platform: platform_name}
|
||||
) %></p>
|
||||
<div class="info-column">
|
||||
<div class="title"><%- gettext('Username') %></div>
|
||||
<div class="data"><%- username %></div>
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<div class="title"><%- gettext('Email address') %></div>
|
||||
<div class="data"><%- email %></div>
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<div class="title"><%- gettext('Legal name') %></div>
|
||||
<div class="data"><%- name %></div>
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<div class="title"><%- gettext('Country of residence') %></div>
|
||||
<div class="data"><%- country %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= fields %>
|
||||
|
||||
<div class="cta-wrapper clearfix">
|
||||
<a href="<%- student_faq_url %>" class="nav-link"><%- interpolate_text(
|
||||
gettext('Back to {platform} FAQs'),
|
||||
{platform: platform_name}
|
||||
) %></a>
|
||||
<button type="submit" class="action action-primary action-update js-submit-form submit-form"><%- gettext("Submit Application") %></button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,8 @@
|
||||
<h1><%- gettext('Financial Assistance Application') %></h1>
|
||||
<p class="js-success-message success-message" tabindex="-1"><%- interpolate_text(
|
||||
gettext('Thank you for submitting your financial assistance application for {course_name}! You can expect a response in 2-4 business days.'), {course_name: course}
|
||||
) %>
|
||||
</p>
|
||||
<div class="cta-wrapper clearfix">
|
||||
<a href="<%- dashboard_url %>" class="btn btn-blue btn-dashboard"><%- gettext('Go to Dashboard') %></a>
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/financial-assistance/models/financial_assistance_model',
|
||||
'text!js/financial-assistance/templates/financial_assessment_form.underscore',
|
||||
'text!js/financial-assistance/templates/financial_assessment_submitted.underscore',
|
||||
'js/student_account/views/FormView',
|
||||
'text!templates/student_account/form_field.underscore'
|
||||
],
|
||||
function(Backbone, $, _, gettext, FinancialAssistanceModel, formViewTpl, successTpl, FormView, formFieldTpl) {
|
||||
return FormView.extend({
|
||||
el: '.financial-assistance-wrapper',
|
||||
events: {
|
||||
'click .js-submit-form': 'submitForm'
|
||||
},
|
||||
tpl: formViewTpl,
|
||||
fieldTpl: formFieldTpl,
|
||||
formType: 'financial-assistance',
|
||||
requiredStr: '',
|
||||
submitButton: '.js-submit-form',
|
||||
|
||||
initialize: function(data) {
|
||||
var context = data.context,
|
||||
fields = context.fields;
|
||||
|
||||
// Add default option to array
|
||||
if ( fields[0].options.length > 1 ) {
|
||||
fields[0].options.unshift({
|
||||
name: '- ' + gettext('Choose one') + ' -',
|
||||
value: '',
|
||||
default: true
|
||||
});
|
||||
}
|
||||
|
||||
// Set non-form data needed to render the View
|
||||
this.context = {
|
||||
dashboard_url: context.dashboard_url,
|
||||
header_text: context.header_text,
|
||||
platform_name: context.platform_name,
|
||||
student_faq_url: context.student_faq_url
|
||||
};
|
||||
|
||||
// Make the value accessible to this View
|
||||
this.user_details = context.user_details;
|
||||
|
||||
// Initialize the model and set user details
|
||||
this.model = new FinancialAssistanceModel({
|
||||
url: context.submit_url
|
||||
});
|
||||
this.model.set( context.user_details );
|
||||
this.listenTo( this.model, 'error', this.saveError );
|
||||
this.model.on('sync', this.renderSuccess, this);
|
||||
|
||||
// Build the form
|
||||
this.buildForm( fields );
|
||||
},
|
||||
|
||||
render: function(html) {
|
||||
var data = _.extend( this.model.toJSON(), this.context, {
|
||||
fields: html || '',
|
||||
});
|
||||
|
||||
this.$el.html(_.template(this.tpl, data));
|
||||
|
||||
this.postRender();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
renderSuccess: function() {
|
||||
this.$el.html(_.template(successTpl, {
|
||||
course: this.model.get('course'),
|
||||
dashboard_url: this.context.dashboard_url
|
||||
}));
|
||||
|
||||
$('.js-success-message').focus();
|
||||
},
|
||||
|
||||
saveError: function(error) {
|
||||
/*jslint maxlen: 500 */
|
||||
var txt = [
|
||||
'An error has occurred. Wait a few minutes and then try to submit the application again.',
|
||||
'If you continue to have issues please contact support.'
|
||||
],
|
||||
msg = gettext(txt.join(' '));
|
||||
|
||||
if (error.status === 0) {
|
||||
msg = gettext('An error has occurred. Check your Internet connection and try again.');
|
||||
}
|
||||
|
||||
this.errors = ['<li>' + msg + '</li>'];
|
||||
this.setErrors();
|
||||
this.element.hide( this.$resetSuccess );
|
||||
this.toggleDisableButton(false);
|
||||
},
|
||||
|
||||
setExtraData: function(data) {
|
||||
return _.extend(data, this.user_details);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,59 @@
|
||||
define([
|
||||
'backbone',
|
||||
'jquery',
|
||||
'js/financial-assistance/views/financial_assistance_form_view'
|
||||
], function (Backbone, $, FinancialAssistanceFormView) {
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Financial Assistance View', function () {
|
||||
var view = null,
|
||||
context = {
|
||||
fields: [{
|
||||
defaultValue: '',
|
||||
form: 'financial-assistance',
|
||||
instructions: 'select a course',
|
||||
label: 'Course',
|
||||
name: 'course',
|
||||
options: [
|
||||
{'name': 'Verified with Audit', 'value': 'course-v1:HCFA+VA101+2015'},
|
||||
{'name': 'Something Else', 'value': 'course-v1:SomethingX+SE101+215'},
|
||||
{'name': 'Test Course', 'value': 'course-v1:TestX+T101+2015'}
|
||||
],
|
||||
placeholder: '',
|
||||
required: true,
|
||||
requiredStr: '',
|
||||
type: 'select'
|
||||
}],
|
||||
user_details: {
|
||||
country: 'UK',
|
||||
email: 'xsy@edx.org',
|
||||
name: 'xsy',
|
||||
username: 'xsy4ever'
|
||||
},
|
||||
header_text: ['Line one.', 'Line two.'],
|
||||
student_faq_url: '/faqs',
|
||||
dashboard_url: '/dashboard',
|
||||
platform_name: 'edx',
|
||||
submit_url: '/api/financial/v1/assistance'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div class="financial-assistance-wrapper"></div>');
|
||||
view = new FinancialAssistanceFormView({
|
||||
el: '.financial-assistance-wrapper',
|
||||
context: context
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
view.undelegateEvents();
|
||||
view.remove();
|
||||
});
|
||||
|
||||
it('should exist', function() {
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -213,6 +213,13 @@
|
||||
this.focusFirstError();
|
||||
},
|
||||
|
||||
/* Allows extended views to add non-form attributes
|
||||
* to the data before saving it to model
|
||||
*/
|
||||
setExtraData: function( data ) {
|
||||
return data;
|
||||
},
|
||||
|
||||
submitForm: function( event ) {
|
||||
var data = this.getFormData();
|
||||
|
||||
@@ -223,6 +230,7 @@
|
||||
this.toggleDisableButton(true);
|
||||
|
||||
if ( !_.compact(this.errors).length ) {
|
||||
data = this.setExtraData( data );
|
||||
this.model.set( data );
|
||||
this.model.save();
|
||||
this.toggleErrorMsg( false );
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
'js/discovery/discovery_factory',
|
||||
'js/edxnotes/views/notes_visibility_factory',
|
||||
'js/edxnotes/views/page_factory',
|
||||
'js/financial-assistance/financial_assistance_form_factory',
|
||||
'js/groups/views/cohorts_dashboard_factory',
|
||||
'js/search/course/course_search_factory',
|
||||
'js/search/dashboard/dashboard_search_factory',
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
%fa-copy {
|
||||
@extend %t-copy-base;
|
||||
padding: ($baseline/2) 0;
|
||||
margin: 0;
|
||||
color: $m-gray-d2;
|
||||
};
|
||||
|
||||
.financial-assistance-wrapper {
|
||||
margin: auto;
|
||||
padding: $baseline 0;
|
||||
padding: $baseline ($baseline/2);
|
||||
max-width: 1180px;
|
||||
|
||||
.financial-assistance {
|
||||
h1 {
|
||||
@extend %t-title4;
|
||||
@include text-align(left);
|
||||
margin: 0;
|
||||
padding: ($baseline/2) 0;
|
||||
border-bottom: 4px solid $gray-l5;
|
||||
color: $m-gray-d3;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@extend %t-title6;
|
||||
@extend %t-strong;
|
||||
margin-top: ($baseline/2);
|
||||
text-transform: none;
|
||||
}
|
||||
p {
|
||||
@extend %fa-copy;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
p {
|
||||
@extend %t-copy-base;
|
||||
padding: ($baseline/2) 0;
|
||||
margin: 0;
|
||||
color: $m-gray-d2;
|
||||
}
|
||||
.financial-assistance {
|
||||
padding-bottom: ($baseline/2);
|
||||
border-bottom: 4px solid $gray-l5;
|
||||
|
||||
.apply-form-list {
|
||||
padding: 0;
|
||||
@@ -73,4 +79,165 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Application form View
|
||||
.intro {
|
||||
border-bottom: 4px solid $gray-l5;
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dashboard {
|
||||
@include float(right);
|
||||
color: $white;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@include clearfix();
|
||||
border-bottom: 2px solid $gray-l5;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.info-column {
|
||||
@include float(left);
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %fa-copy;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
@extend %fa-copy;
|
||||
padding: 0;
|
||||
color: $black;
|
||||
font-size: 1.125em;
|
||||
}
|
||||
}
|
||||
|
||||
.financial-assistance-form {
|
||||
@extend .login-register;
|
||||
|
||||
.action-primary {
|
||||
@include float(left);
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
margin: 15px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
form {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
select,
|
||||
input {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
input {
|
||||
border: {
|
||||
top: none;
|
||||
right: none;
|
||||
bottom: 3px solid $gray-l1;
|
||||
left: none;
|
||||
};
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
|
||||
& + label {
|
||||
@include margin-left(30px);
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cta-wrapper {
|
||||
border-top: 4px solid $gray-l5;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
@include media($bp-medium) {
|
||||
.user-info {
|
||||
.info-column {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.financial-assistance-form {
|
||||
.action-primary {
|
||||
@include float(right);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media($bp-large) {
|
||||
.user-info {
|
||||
.info-column {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.financial-assistance-form {
|
||||
.action-primary {
|
||||
@include float(right);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media($bp-huge) {
|
||||
.user-info {
|
||||
.info-column {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.financial-assistance-form {
|
||||
.action-primary {
|
||||
@include float(right);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,3 @@ ${fragment.foot_html()}
|
||||
</nav>
|
||||
|
||||
<%include file="../modal/accessible_confirm.html" />
|
||||
|
||||
## No footer in chromeless
|
||||
<%block name="footer"></%block>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -79,7 +79,6 @@ from branding import api as branding_api
|
||||
% else:
|
||||
<%static:js group='main_vendor'/>
|
||||
<%static:js group='application'/>
|
||||
<%static:js group='module-js'/>
|
||||
% endif
|
||||
|
||||
<script>
|
||||
@@ -92,6 +91,10 @@ from branding import api as branding_api
|
||||
</script>
|
||||
<script type="text/javascript" src="${static.url("lms/js/require-config.js")}"></script>
|
||||
|
||||
% if not disable_courseware_js:
|
||||
<%static:js group='module-js'/>
|
||||
% endif
|
||||
|
||||
<%block name="headextra"/>
|
||||
|
||||
<%static:optional_include_mako file="head-extra.html" with_microsite="True" />
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<option value="<%= el.value%>"<% if ( el.default ) { %> data-isdefault="true"<% } %>><%= el.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<% } else if ( type === 'textarea' ) { %>
|
||||
<textarea id="<%= form %>-<%= name %>"
|
||||
type="<%= type %>"
|
||||
@@ -35,6 +36,7 @@
|
||||
<% });
|
||||
} %>
|
||||
<% if ( required ) { %> aria-required="true" required<% } %> ></textarea>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<% } else { %>
|
||||
<input id="<%= form %>-<%= name %>"
|
||||
type="<%= type %>"
|
||||
@@ -52,16 +54,15 @@
|
||||
<% if ( placeholder ) { %> placeholder="<%= placeholder %>"<% } %>
|
||||
value="<%- defaultValue %>"
|
||||
/>
|
||||
<% if ( type === 'checkbox' ) { %>
|
||||
<label for="<%= form %>-<%= name %>">
|
||||
<%= label %>
|
||||
<% if ( required && requiredStr ) { %> <%= requiredStr %><% } %>
|
||||
</label>
|
||||
<% } %>
|
||||
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
|
||||
<% } %>
|
||||
|
||||
<% if ( type === 'checkbox' ) { %>
|
||||
<label for="<%= form %>-<%= name %>">
|
||||
<%= label %>
|
||||
<% if ( required && requiredStr ) { %> <%= requiredStr %><% } %>
|
||||
</label>
|
||||
<% } %>
|
||||
|
||||
<% if( form === 'login' && name === 'password' ) { %>
|
||||
<a href="#" class="forgot-password field-link"><%- gettext("Forgot password?") %></a>
|
||||
<% } %>
|
||||
|
||||
Reference in New Issue
Block a user