Merged master to rc/2013-11-21
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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']}"
|
||||
|
||||
Reference in New Issue
Block a user