Merged master to rc/2013-11-21

This commit is contained in:
Ned Batchelder
2013-11-25 14:56:09 -05:00
115 changed files with 2799 additions and 1575 deletions

View File

@@ -5,7 +5,7 @@ import datetime
from pytz import UTC
from django.conf import settings
from django.test.utils import override_settings
from django.test.client import RequestFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import editable_modulestore
from xmodule.modulestore.tests.factories import CourseFactory
@@ -25,6 +25,7 @@ class AnonymousIndexPageTest(ModuleStoreTestCase):
"""
def setUp(self):
self.store = editable_modulestore()
self.factory = RequestFactory()
self.course = CourseFactory.create()
self.course.days_early_for_beta = 5
self.course.enrollment_start = datetime.datetime.now(UTC) + datetime.timedelta(days=3)
@@ -32,7 +33,11 @@ class AnonymousIndexPageTest(ModuleStoreTestCase):
@override_settings(MITX_FEATURES=MITX_FEATURES_WITH_STARTDATE)
def test_none_user_index_access_with_startdate_fails(self):
with self.assertRaises(Exception):
"""
This was a "before" test for a bugfix. If someone fixes the bug another way in the future
and this test begins failing (but the other two pass), then feel free to delete this test.
"""
with self.assertRaisesRegexp(AttributeError, "'NoneType' object has no attribute 'is_authenticated'"):
student.views.index(self.factory.get('/'), user=None) # pylint: disable=E1101
@override_settings(MITX_FEATURES=MITX_FEATURES_WITH_STARTDATE)

View File

@@ -93,6 +93,7 @@ class Command(BaseCommand):
total = enrolled_students.count()
count = 0
start = datetime.datetime.now(UTC)
for student in enrolled_students:
count += 1
if count % STATUS_INTERVAL == 0:

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'GeneratedCertificate.mode'
db.add_column('certificates_generatedcertificate', 'mode',
self.gf('django.db.models.fields.CharField')(default='honor', max_length=32),
keep_default=False)
def backwards(self, orm):
# Deleting field 'GeneratedCertificate.mode'
db.delete_column('certificates_generatedcertificate', 'mode')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'certificates.certificatewhitelist': {
'Meta': {'object_name': 'CertificateWhitelist'},
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'whitelist': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'certificates.generatedcertificate': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '32'}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['certificates']

View File

@@ -1,6 +1,7 @@
from django.contrib.auth.models import User
from django.db import models
from datetime import datetime
from model_utils import Choices
"""
Certificates are created for a student and an offering of a course.
@@ -62,7 +63,6 @@ class CertificateStatuses(object):
restricted = 'restricted'
unavailable = 'unavailable'
class CertificateWhitelist(models.Model):
"""
Tracks students who are whitelisted, all users
@@ -86,11 +86,13 @@ class GeneratedCertificate(models.Model):
key = models.CharField(max_length=32, blank=True, default='')
distinction = models.BooleanField(default=False)
status = models.CharField(max_length=32, default='unavailable')
MODES = Choices('verified', 'honor', 'audit')
mode = models.CharField(max_length=32, choices=MODES, default=MODES.honor)
name = models.CharField(blank=True, max_length=255)
created_date = models.DateTimeField(
auto_now_add=True, default=datetime.now)
auto_now_add=True, default=datetime.now)
modified_date = models.DateTimeField(
auto_now=True, default=datetime.now)
auto_now=True, default=datetime.now)
error_reason = models.CharField(max_length=512, blank=True, default='')
class Meta:
@@ -128,8 +130,9 @@ def certificate_status_for_student(student, course_id):
try:
generated_certificate = GeneratedCertificate.objects.get(
user=student, course_id=course_id)
d = {'status': generated_certificate.status}
user=student, course_id=course_id)
d = {'status': generated_certificate.status,
'mode': generated_certificate.mode}
if generated_certificate.grade:
d['grade'] = generated_certificate.grade
if generated_certificate.status == CertificateStatuses.downloadable:
@@ -138,4 +141,4 @@ def certificate_status_for_student(student, course_id):
return d
except GeneratedCertificate.DoesNotExist:
pass
return {'status': CertificateStatuses.unavailable}
return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor}

View File

@@ -9,7 +9,8 @@ from capa.xqueue_interface import XQueueInterface
from capa.xqueue_interface import make_xheader, make_hashkey
from django.conf import settings
from requests.auth import HTTPBasicAuth
from student.models import UserProfile
from student.models import UserProfile, CourseEnrollment
from verify_student.models import SoftwareSecurePhotoVerification
import json
import random
@@ -57,7 +58,7 @@ class XQueueCertInterface(object):
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
requests_auth = HTTPBasicAuth(
*settings.XQUEUE_INTERFACE['basic_auth'])
*settings.XQUEUE_INTERFACE['basic_auth'])
else:
requests_auth = None
@@ -68,10 +69,10 @@ class XQueueCertInterface(object):
self.request = request
self.xqueue_interface = XQueueInterface(
settings.XQUEUE_INTERFACE['url'],
settings.XQUEUE_INTERFACE['django_auth'],
requests_auth,
)
settings.XQUEUE_INTERFACE['url'],
settings.XQUEUE_INTERFACE['django_auth'],
requests_auth,
)
self.whitelist = CertificateWhitelist.objects.all()
self.restricted = UserProfile.objects.filter(allow_certificate=False)
self.use_https = True
@@ -84,7 +85,7 @@ class XQueueCertInterface(object):
course_id - courseenrollment.course_id (string)
WARNING: this command will leave the old certificate, if one exists,
laying around in AWS taking up space. If this is a problem,
laying around in AWS taking up space. If this is a problem,
take pains to clean up storage before running this command.
Change the certificate status to unavailable (if it exists) and request
@@ -92,7 +93,7 @@ class XQueueCertInterface(object):
Return the status object.
"""
# TODO: when del_cert is implemented and plumbed through certificates
# TODO: when del_cert is implemented and plumbed through certificates
# repo also, do a deletion followed by a creation r/t a simple
# recreation. XXX: this leaves orphan cert files laying around in
# AWS. See note in the docstring too.
@@ -149,13 +150,15 @@ class XQueueCertInterface(object):
"""
VALID_STATUSES = [status.generating,
status.unavailable,
status.deleted,
status.unavailable,
status.deleted,
status.error,
status.notpassing]
cert_status = certificate_status_for_student(student, course_id)['status']
new_status = cert_status
if cert_status in VALID_STATUSES:
# grade the student
@@ -165,9 +168,6 @@ class XQueueCertInterface(object):
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
# Needed
self.request.user = student
self.request.session = {}
@@ -175,45 +175,64 @@ class XQueueCertInterface(object):
grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists()
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
org = course_id.split('/')[0]
course_num = course_id.split('/')[1]
cert_mode = enrollment_mode
if enrollment_mode == GeneratedCertificate.MODES.verified and SoftwareSecurePhotoVerification.user_is_verified(student):
template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
org, course_num)
elif (enrollment_mode == GeneratedCertificate.MODES.verified and not
SoftwareSecurePhotoVerification.user_is_verified(student)):
template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num)
cert_mode = GeneratedCertificate.MODES.honor
else:
# honor code and audit students
template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num)
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
cert.mode = cert_mode
cert.user = student
cert.grade = grade['percent']
cert.course_id = course_id
cert.name = profile.name
if is_whitelisted or grade['grade'] is not None:
key = make_hashkey(random.random())
cert.grade = grade['percent']
cert.user = student
cert.course_id = course_id
cert.key = key
cert.name = profile.name
# check to see whether the student is on the
# the embargoed country restricted list
# otherwise, put a new certificate request
# on the queue
if self.restricted.filter(user=student).exists():
cert.status = status.restricted
new_status = status.restricted
cert.status = new_status
cert.save()
else:
key = make_hashkey(random.random())
cert.key = key
contents = {
'action': 'create',
'username': student.username,
'course_id': course_id,
'name': profile.name,
'grade': grade['grade'],
'template_pdf': template_pdf,
}
cert.status = status.generating
new_status = status.generating
cert.status = new_status
cert.save()
self._send_to_xqueue(contents, key)
else:
cert_status = status.notpassing
cert.grade = grade['percent']
cert.user = student
cert.course_id = course_id
cert.name = profile.name
cert.status = cert_status
new_status = status.notpassing
cert.status = new_status
cert.save()
return cert_status
return new_status
def _send_to_xqueue(self, contents, key):
@@ -227,7 +246,7 @@ class XQueueCertInterface(object):
proto, settings.SITE_NAME, key), key, settings.CERT_QUEUE)
(error, msg) = self.xqueue_interface.send_to_queue(
header=xheader, body=json.dumps(contents))
header=xheader, body=json.dumps(contents))
if error:
logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
raise Exception('Unable to send queue message')

View File

@@ -335,7 +335,6 @@ def _progress_summary(student, request, course):
module_creator = section_module.xmodule_runtime.get_module
for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator):
course_id = course.id
(correct, total) = get_score(course_id, student, module_descriptor, module_creator)
if correct is None and total is None:

View File

@@ -134,8 +134,8 @@ class Migration(SchemaMigration):
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'value': ('django.db.models.fields.TextField', [], {'default': "'null'"})
},
'courseware.xmoduleuserstatesummary': {
'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleUserStateSummary'},
'courseware.xmoduleuserstatesummaryfield': {
'Meta': {'unique_together': "(('usage_id', 'field_name'),)", 'object_name': 'XModuleUserStateSummaryField'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'usage_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'field_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),

View File

@@ -422,6 +422,8 @@ class CertificateItem(OrderItem):
return
target_cert.status = 'refunded'
target_cert.save()
target_cert.order.status = 'refunded'
target_cert.order.save()
order_number = target_cert.order_id
# send billing an email so they can handle refunding

View File

@@ -373,6 +373,7 @@ class CertificateItemTest(ModuleStoreTestCase):
CourseEnrollment.unenroll(self.user, self.course_id)
target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified')
self.assertTrue(target_certs[0])
self.assertEquals(target_certs[0].order.status, 'refunded')
def test_refund_cert_callback_before_expiration(self):
# If the expiration date has not yet passed on a verified mode, the user can be refunded
@@ -395,6 +396,7 @@ class CertificateItemTest(ModuleStoreTestCase):
CourseEnrollment.unenroll(self.user, course_id)
target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified')
self.assertTrue(target_certs[0])
self.assertEquals(target_certs[0].order.status, 'refunded')
@patch('shoppingcart.models.log.error')
def test_refund_cert_callback_before_expiration_email_error(self, error_logger):

View File

@@ -261,7 +261,13 @@ CC_PROCESSOR = AUTH_TOKENS.get('CC_PROCESSOR', CC_PROCESSOR)
SECRET_KEY = AUTH_TOKENS['SECRET_KEY']
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
if AWS_ACCESS_KEY_ID == "":
AWS_ACCESS_KEY_ID = None
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
if AWS_SECRET_ACCESS_KEY == "":
AWS_SECRET_ACCESS_KEY = None
AWS_STORAGE_BUCKET_NAME = AUTH_TOKENS.get('AWS_STORAGE_BUCKET_NAME', 'edxuploads')
DATABASES = AUTH_TOKENS['DATABASES']

View File

@@ -7,7 +7,7 @@
<input type="text" id="calculator_input" tabindex="-1" />
<div class="help-wrapper">
<a id="calculator_hint" href="#" role="button" aria-haspopup="true" aria-controls="calculator_input_help" aria-expanded="false" tabindex="-1">Hints</a>
<dl id="calculator_input_help" class="help"></dl>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true"></div>
</div>
</div>
<input id="calculator_button" type="submit" title="Calculate" arial-label="Calculate" value="=" tabindex="-1" />

View File

@@ -55,11 +55,26 @@ describe 'Calculator', ->
it 'show the help overlay', ->
@calculator.helpShow()
expect($('.help')).toHaveClass('shown')
expect($('.help')).toHaveAttr('aria-hidden', 'false')
describe 'helpHide', ->
it 'show the help overlay', ->
@calculator.helpHide()
expect($('.help')).not.toHaveClass('shown')
expect($('.help')).toHaveAttr('aria-hidden', 'true')
describe 'handleKeyDown', ->
it 'on pressing Esc the hint becomes hidden', ->
@calculator.helpShow()
e = jQuery.Event('keydown', { which: 27 } );
$(document).trigger(e);
expect($('.help')).not.toHaveClass 'shown'
it 'On pressing other buttons the hint continue to show', ->
@calculator.helpShow()
e = jQuery.Event('keydown', { which: 32 } );
$(document).trigger(e);
expect($('.help')).toHaveClass 'shown'
describe 'calculate', ->
beforeEach ->

View File

@@ -10,6 +10,9 @@ class @Calculator
)
.click (e) ->
e.preventDefault()
$(document).keydown $.proxy(@handleKeyDown, @)
$('div.help-wrapper')
.focusin($.proxy @helpOnFocus, @)
.focusout($.proxy @helpOnBlur, @)
@@ -24,14 +27,14 @@ class @Calculator
$('div.calc-main').toggleClass 'open'
if $calc.hasClass('closed')
$calcWrapper
.find('input, a, dt, dd')
.find('input, a')
.attr 'tabindex', -1
else
text = gettext('Close Calculator')
isExpanded = true
$calcWrapper
.find('input, a, dt, dd')
.find('input, a,')
.attr 'tabindex', 0
# TODO: Investigate why doing this without the timeout causes it to jump
# down to the bottom of the page. I suspect it's because it's putting the
@@ -57,13 +60,21 @@ class @Calculator
@helpHide()
helpShow: ->
$('.help').addClass 'shown'
$('#calculator_hint').attr 'aria-expanded', true
$('.help')
.addClass('shown')
.attr('aria-hidden', false)
helpHide: ->
if not @isFocusedHelp
$('.help').removeClass 'shown'
$('#calculator_hint').attr 'aria-expanded', false
$('.help')
.removeClass('shown')
.attr('aria-hidden', true)
handleKeyDown: (e) ->
ESC = 27
if e.which is ESC and $('.help').hasClass 'shown'
@isFocusedHelp = false
@helpHide()
calculate: ->
$.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) ->

View File

@@ -120,39 +120,48 @@ div.calc-main {
display: block;
}
dl {
.help {
background: #fff;
border-radius: 3px;
box-shadow: 0 0 3px #999;
color: #333;
opacity: 0;
padding: 10px;
position: absolute;
right: -40px;
top: -122px;
bottom: 57px;
@include transition(none);
width: 600px;
height: 0;
overflow: hidden;
pointer-events: none;
display: none;
&.shown {
height: auto;
opacity: 1;
overflow: visible;
display: block;
pointer-events: auto;
}
dt {
clear: both;
float: left;
.bold {
font-weight: bold;
padding-right: 12px;
}
dd {
float: left;
margin-left: 0;
p, p+p {
margin: 0;
}
table {
margin: 10px 0;
td, th {
padding: 2px 5px;
}
}
.calc-postfixes {
margin: 10px auto;
td, th {
padding: 2px 15px;
}
}
}
}

View File

@@ -218,24 +218,113 @@ ${fragment.foot_html()}
<input type="text" id="calculator_input" title="${_('Calculator Input Field')}" tabindex="-1" />
<div class="help-wrapper">
<a id="calculator_hint" href="#" role="button" aria-haspopup="true" aria-controls="calculator_input_help" aria-expanded="false" tabindex="-1">${_("Hints")}</a>
<dl id="calculator_input_help" class="help">
<dt tabindex="-1">${_("Suffixes:")}</dt>
<dd tabindex="-1"> %kMGTcmunp</dd>
<dt tabindex="-1">${_("Operations:")}</dt>
<dd tabindex="-1">^ * / + - ()</dd>
<dt tabindex="-1">${_("Functions:")}</dt>
<dd tabindex="-1">sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs </dd>
<dt tabindex="-1">${_("Constants")}</dt>
<dd tabindex="-1">e, pi</dd>
<!-- Students won't know what parallel means at this time. Complex numbers aren't well tested in the courseware, so we would prefer to not expose them. If you read the comments in the source, feel free to use them. If you run into a bug, please let us know. But we can't officially support them right now.
<dt>Unsupported:</dt> <dd>||, j </dd> -->
</dl>
<a id="calculator_hint" href="#" role="button" aria-describedby="calculator_input_help" tabindex="-1">${_("Hints")}</a>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true">
<p><span class="bold">${_("Integers")}:</span> 2520</p>
<p><span class="bold">${_("Decimals")}:</span> 3.14 or .98</p>
<p><span class="bold">${_("Scientific notation")}:</span> 1.2e-2 (=0.012), -4.4e+5 = -4.4e5 (=-440,000)</p>
<p><span class="bold">${_("Appending SI postfixes")}:</span> 2.25k (=2,250)</p>
<p><span class="bold">${_("Supported SI postfixes")}:</span></p>
<table class="calc-postfixes">
<tbody>
<tr>
<td>%</td>
<td>percent</td>
<td>0.01 = 1e-2</td>
</tr>
<tr>
<td>T</td>
<td>tera</td>
<td>1e12</td>
</tr>
<tr>
<td>G</td>
<td>giga</td>
<td>1e9</td>
</tr>
<tr>
<td>M</td>
<td>mega</td>
<td>1e6</td>
</tr>
<tr>
<td>k</td>
<td>kilo</td>
<td>1000 = 1e3</td>
</tr>
<tr>
<td>c</td>
<td>centi</td>
<td>0.01 = 1e-2</td>
</tr>
<tr>
<td>m</td>
<td>milli</td>
<td>0.001 = 1e-3</td>
</tr>
<tr>
<td>u</td>
<td>micro</td>
<td>1e-6</td>
</tr>
<tr>
<td>n</td>
<td>nano</td>
<td>1e-9</td>
</tr>
<tr>
<td>p</td>
<td>pico</td>
<td>1e-12</td>
</tr>
</tbody>
</table>
<p><span class="bold">${_("Operators")}:</span> + - * / ^ and || (${_("parallel resistors function")})</p>
<p><span class="bold">${_("Functions")}:</span> sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs, fact/factorial</p>
<p><span class="bold">${_("Constants")}:</span></p>
<table>
<tbody>
<tr>
<td>j</td>
<td>=</td>
<td>sqrt(-1)</td>
</tr>
<tr>
<td>e</td>
<td>=</td>
<td>${_("Euler's number")}</td>
</tr>
<tr>
<td>pi</td>
<td>=</td>
<td>${_("ratio of a circle's circumference to it's diameter")}</td>
</tr>
<tr>
<td>k</td>
<td>=</td>
<td>${_("Boltzmann constant")}</td>
</tr>
<tr>
<td>c</td>
<td>=</td>
<td>${_("speed of light")}</td>
</tr>
<tr>
<td>T</td>
<td>=</td>
<td>${_("freezing point of water in degrees Kelvin")}</td>
</tr>
<tr>
<td>q</td>
<td>=</td>
<td>${_("fundamental charge")}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<input id="calculator_button" type="submit" title="${_('Calculate')}" value="=" arial-label="${_('Calculate')}" value="=" tabindex="-1" />
<input id="calculator_button" type="submit" title="${_('Calculate')}" value="=" aria-label="${_('Calculate')}" value="=" tabindex="-1" />
<input type="text" id="calculator_output" title="${_('Calculator Output Field')}" readonly tabindex="-1" />
</form>

View File

@@ -19,7 +19,7 @@ else:
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
<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':
% if cert_status['status'] == 'notpassing' and enrollment.mode != 'audit':
${_("Grade required for a certificate:")} <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% elif cert_status['status'] == 'restricted' and enrollment.mode == 'verified':
@@ -44,6 +44,12 @@ else:
<a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}">
${_("Download Your Certificate (PDF)")}</a></li>
% elif cert_status['show_download_url'] and enrollment.mode == 'verified' and cert_status['mode'] == 'honor':
<li class="action">
<p>${_('Since we did not have a valid set of verification photos from you when certificates were generated, we could not grant you a verified certificate. An honor code certificate has been granted instead.')}</p>
<a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}">
${_("Download Your Certificate (PDF)")}</a></li>
% elif cert_status['show_download_url'] and enrollment.mode == 'verified':
<li class="action">
<a class="btn" href="${cert_status['download_url']}"