Commit f7424eb8 by Diana Huang

Merge branch 'master' into tests/diana/update-oe-unit-tests

parents 585f1c41 3669ea5c
......@@ -3,6 +3,7 @@
"/static/js/vendor/jquery.min.js",
"/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js"
"/static/js/vendor/backbone-min.js",
"/static/js/vendor/RequireJS.js"
]
}
......@@ -87,7 +87,10 @@ $(document).ready(function() {
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
stack: '.unit',
zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid"
});
......@@ -95,7 +98,10 @@ $(document).ready(function() {
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
stack: '.id-holder',
zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid"
});
......@@ -179,10 +185,12 @@ function toggleSections(e) {
if($button.hasClass('is-activated')) {
$section.addClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('collapsed').addClass('expand');
// first child in order to avoid the icons on the subsection lists which are not in the first child
$section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand');
} else {
$section.removeClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('expand').addClass('collapse');
// first child in order to avoid the icons on the subsection lists which are not in the first child
$section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse');
}
}
......@@ -271,9 +279,67 @@ function removePolicyMetadata(e) {
saveSubsection()
}
CMS.HesitateEvent.toggleXpandHesitation = null;
function initiateHesitate(event, ui) {
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
$('.collapsed').each(function() {
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
// reset b/c these were holding values from aborts
this.isover = false;
});
}
function checkHoverState(event, ui) {
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
var draggable = $(this).data("ui-draggable"),
x1 = (draggable.positionAbs || draggable.position.absolute).left + (draggable.helperProportions.width / 2),
y1 = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
$('.collapsed').each(function() {
// don't expand the thing being carried
if (ui.helper.is(this)) {
return;
}
$.extend(this, {offset : $(this).offset()});
var droppable = this,
l = droppable.offset.left,
r = l + droppable.proportions.width,
t = droppable.offset.top,
b = t + droppable.proportions.height;
if (l === r) {
// probably wrong values b/c invisible at the time of caching
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
r = l + droppable.proportions.width;
b = t + droppable.proportions.height;
}
// equivalent to the intersects test
var intersects = (l < x1 && // Right Half
x1 < r && // Left Half
t < y1 && // Bottom Half
y1 < b ), // Top Half
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
if(!c) {
return;
}
this[c] = true;
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
}
function removeHesitate(event, ui) {
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
CMS.HesitateEvent.toggleXpandHesitation = null;
}
function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed');
$(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse');
$(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
......
......@@ -18,33 +18,31 @@ CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) {
this.timeoutEventId = null;
this.originalEvent = null;
this.onlyOnce = (onlyOnce === true);
}
};
CMS.HesitateEvent.DURATION = 400;
CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) {
console.log('trigger');
if (this.timeoutEventId === null) {
this.timeoutEventId = window.setTimeout(this.fireEvent, CMS.HesitateEvent.DURATION);
this.originalEvent = event;
// is it wrong to bind to the below v $(event.currentTarget)?
$(this.originalEvent.delegateTarget).on(this.cancelSelector, this.untrigger);
if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION);
event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
}
}
};
CMS.HesitateEvent.prototype.fireEvent = function(event) {
console.log('fire');
this.timeoutEventId = null;
$(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger);
if (this.onlyOnce) $(this.originalEvent.delegateTarget).off(this.originalEvent.type, this.trigger);
this.executeOnTimeOut(this.originalEvent);
}
event.data.timeoutEventId = null;
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger);
event.data.executeOnTimeOut(event.data.originalEvent);
};
CMS.HesitateEvent.prototype.untrigger = function(event) {
console.log('untrigger');
if (this.timeoutEventId) {
window.clearTimeout(this.timeoutEventId);
$(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger);
if (event.data.timeoutEventId) {
window.clearTimeout(event.data.timeoutEventId);
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
}
this.timeoutEventId = null;
}
\ No newline at end of file
event.data.timeoutEventId = null;
};
\ No newline at end of file
......@@ -10,6 +10,25 @@ from course_groups.cohorts import (get_cohort, get_course_cohorts,
from xmodule.modulestore.django import modulestore, _MODULESTORES
# NOTE: running this with the lms.envs.test config works without
# manually overriding the modulestore. However, running with
# cms.envs.test doesn't.
def xml_store_config(data_dir):
return {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase):
......@@ -77,7 +96,7 @@ class TestCohorts(django.test.TestCase):
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
self.assertFalse(course.is_cohorted)
user = User.objects.create(username="test", email="a@b.com")
other_user = User.objects.create(username="test2", email="a2@b.com")
......
from django.core.management.base import BaseCommand, CommandError
import os
from optparse import make_option
from student.models import UserProfile
import csv
class Command(BaseCommand):
help = """
Sets or gets certificate restrictions for users
from embargoed countries. (allow_certificate in
userprofile)
CSV should be comma delimited with double quoted entries.
$ ... cert_restriction --import path/to/userlist.csv
Export a list of students who have "allow_certificate" in
userprofile set to True
$ ... cert_restriction --output path/to/export.csv
Enable a single user so she is not on the restricted list
$ ... cert_restriction -e user
Disable a single user so she is on the restricted list
$ ... cert_restriction -d user
"""
option_list = BaseCommand.option_list + (
make_option('-i', '--import',
metavar='IMPORT_FILE',
dest='import',
default=False,
help='csv file to import, comma delimitted file with '
'double-quoted entries'),
make_option('-o', '--output',
metavar='EXPORT_FILE',
dest='output',
default=False,
help='csv file to export'),
make_option('-e', '--enable',
metavar='STUDENT',
dest='enable',
default=False,
help="enable a single student's certificate"),
make_option('-d', '--disable',
metavar='STUDENT',
dest='disable',
default=False,
help="disable a single student's certificate")
)
def handle(self, *args, **options):
if options['output']:
if os.path.exists(options['output']):
raise CommandError("File {0} already exists".format(
options['output']))
disabled_users = UserProfile.objects.filter(
allow_certificate=False)
with open(options['output'], 'w') as csvfile:
csvwriter = csv.writer(csvfile, delimiter=',', quotechar='"',
quoting=csv.QUOTE_MINIMAL)
for user in disabled_users:
csvwriter.writerow([user.user.username])
elif options['import']:
if not os.path.exists(options['import']):
raise CommandError("File {0} does not exist".format(
options['import']))
print "Importing students from {0}".format(options['import'])
students = None
with open(options['import']) as csvfile:
student_list = csv.reader(csvfile, delimiter=',',
quotechar='"')
students = [student[0] for student in student_list]
if not students:
raise CommandError(
"Unable to read student data from {0}".format(
options['import']))
UserProfile.objects.filter(user__username__in=students).update(
allow_certificate=False)
elif options['enable']:
print "Enabling {0} for certificate download".format(
options['enable'])
cert_allow = UserProfile.objects.get(
user__username=options['enable'])
cert_allow.allow_certificate = True
cert_allow.save()
elif options['disable']:
print "Disabling {0} for certificate download".format(
options['disable'])
cert_allow = UserProfile.objects.get(
user__username=options['disable'])
cert_allow.allow_certificate = False
cert_allow.save()
from optparse import make_option
from json import dump
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterRegistration
class Command(BaseCommand):
args = '<output JSON file>'
help = """
Dump information as JSON from TestCenterRegistration tables, including username and status.
"""
option_list = BaseCommand.option_list + (
make_option('--course_id',
action='store',
dest='course_id',
help='Specify a particular course.'),
make_option('--exam_series_code',
action='store',
dest='exam_series_code',
default=None,
help='Specify a particular exam, using the Pearson code'),
make_option('--accommodation_pending',
action='store_true',
dest='accommodation_pending',
default=False,
),
)
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError("Missing single argument: output JSON file")
# get output location:
outputfile = args[0]
# construct the query object to dump:
registrations = TestCenterRegistration.objects.all()
if 'course_id' in options and options['course_id']:
registrations = registrations.filter(course_id=options['course_id'])
if 'exam_series_code' in options and options['exam_series_code']:
registrations = registrations.filter(exam_series_code=options['exam_series_code'])
# collect output:
output = []
for registration in registrations:
if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending:
continue
record = {'username' : registration.testcenter_user.user.username,
'email' : registration.testcenter_user.email,
'first_name' : registration.testcenter_user.first_name,
'last_name' : registration.testcenter_user.last_name,
'client_candidate_id' : registration.client_candidate_id,
'client_authorization_id' : registration.client_authorization_id,
'course_id' : registration.course_id,
'exam_series_code' : registration.exam_series_code,
'accommodation_request' : registration.accommodation_request,
'accommodation_code' : registration.accommodation_code,
'registration_status' : registration.registration_status(),
'demographics_status' : registration.demographics_status(),
'accommodation_status' : registration.accommodation_status(),
}
if len(registration.upload_error_message) > 0:
record['registration_error'] = registration.upload_error_message
if registration.needs_uploading:
record['needs_uploading'] = True
output.append(record)
# dump output:
with open(outputfile, 'w') as outfile:
dump(output, outfile)
......@@ -65,7 +65,7 @@ class Command(BaseCommand):
else:
try:
registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile)
Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile.name)
# now update the record:
registration.upload_status = row['Status']
registration.upload_error_message = row['Message']
......
......@@ -179,7 +179,8 @@ class Command(BaseCommand):
if (len(form.errors) > 0):
print "Field Form errors encountered:"
for fielderror in form.errors:
print "Field Form Error: %s" % fielderror
for msg in form.errors[fielderror]:
print "Field Form Error: {} -- {}".format(fielderror, msg)
if (len(form.non_field_errors()) > 0):
print "Non-field Form errors encountered:"
for nonfielderror in form.non_field_errors:
......
......@@ -90,6 +90,7 @@ class UserProfile(models.Model):
)
mailing_address = models.TextField(blank=True, null=True)
goals = models.TextField(blank=True, null=True)
allow_certificate = models.BooleanField(default=1)
def get_meta(self):
js_str = self.meta
......@@ -255,9 +256,9 @@ class TestCenterUserForm(ModelForm):
def clean_country(self):
code = self.cleaned_data['country']
if code and len(code) != 3:
if code and (len(code) != 3 or not code.isalpha()):
raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG')
return code
return code.upper()
def clean(self):
def _can_encode_as_latin(fieldvalue):
......@@ -387,6 +388,12 @@ class TestCenterRegistration(models.Model):
return 'Update'
elif self.uploaded_at is None:
return 'Add'
elif self.registration_is_rejected:
# Assume that if the registration was rejected before,
# it is more likely this is the (first) correction
# than a second correction in flight before the first was
# processed.
return 'Add'
else:
# TODO: decide what to send when we have uploaded an initial version,
# but have not received confirmation back from that upload. If the
......@@ -400,13 +407,14 @@ class TestCenterRegistration(models.Model):
@property
def exam_authorization_count(self):
# TODO: figure out if this should really go in the database (with a default value).
# Someday this could go in the database (with a default value). But at present,
# we do not expect anyone to be authorized to take an exam more than once.
return 1
@property
def needs_uploading(self):
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
@classmethod
def create(cls, testcenter_user, exam, accommodation_request):
registration = cls(testcenter_user = testcenter_user)
......@@ -499,6 +507,33 @@ class TestCenterRegistration(models.Model):
def registration_signup_url(self):
return settings.PEARSONVUE_SIGNINPAGE_URL
def demographics_status(self):
if self.demographics_is_accepted:
return "Accepted"
elif self.demographics_is_rejected:
return "Rejected"
else:
return "Pending"
def accommodation_status(self):
if self.accommodation_is_skipped:
return "Skipped"
elif self.accommodation_is_accepted:
return "Accepted"
elif self.accommodation_is_rejected:
return "Rejected"
else:
return "Pending"
def registration_status(self):
if self.registration_is_accepted:
return "Accepted"
elif self.registration_is_rejected:
return "Rejected"
else:
return "Pending"
class TestCenterRegistrationForm(ModelForm):
class Meta:
model = TestCenterRegistration
......@@ -518,7 +553,15 @@ class TestCenterRegistrationForm(ModelForm):
registration.save()
log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
# TODO: add validation code for values added to accommodation_code field.
def clean_accommodation_code(self):
code = self.cleaned_data['accommodation_code']
if code:
code = code.upper()
codes = code.split('*')
for codeval in codes:
if codeval not in ACCOMMODATION_CODE_DICT:
raise forms.ValidationError(u'Invalid accommodation code specified: "{}"'.format(codeval))
return code
......@@ -532,7 +575,7 @@ def get_testcenter_registration(user, course_id, exam_series_code):
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
get_testcenter_registration.__test__ = False
def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
......
......@@ -135,7 +135,7 @@ def cert_info(user, course):
Get the certificate info needed to render the dashboard section for the given
student and course. Returns a dictionary with keys:
'status': one of 'generating', 'ready', 'notpassing', 'processing'
'status': one of 'generating', 'ready', 'notpassing', 'processing', 'restricted'
'show_download_url': bool
'download_url': url, only present if show_download_url is True
'show_disabled_download_button': bool -- true if state is 'generating'
......@@ -168,6 +168,7 @@ def _cert_info(user, course, cert_status):
CertificateStatuses.regenerating: 'generating',
CertificateStatuses.downloadable: 'ready',
CertificateStatuses.notpassing: 'notpassing',
CertificateStatuses.restricted: 'restricted',
}
status = template_state.get(cert_status['status'], default_status)
......@@ -176,7 +177,7 @@ def _cert_info(user, course, cert_status):
'show_download_url': status == 'ready',
'show_disabled_download_button': status == 'generating',}
if (status in ('generating', 'ready', 'notpassing') and
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
course.end_of_course_survey_url is not None):
d.update({
'show_survey_button': True,
......@@ -192,7 +193,7 @@ def _cert_info(user, course, cert_status):
else:
d['download_url'] = cert_status['download_url']
if status in ('generating', 'ready', 'notpassing'):
if status in ('generating', 'ready', 'notpassing', 'restricted'):
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).
......
......@@ -17,6 +17,7 @@
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/RequireJS.js"></script>
<script type="text/javascript">
AjaxPrefix.addAjaxPrefix(jQuery, function() {
......
......@@ -247,7 +247,7 @@ def remap_namespace(module, target_location_namespace):
return module
def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category):
def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0
parents = []
......@@ -324,11 +324,18 @@ def perform_xlint(data_dir, course_dirs,
for course_id in module_store.modules.keys():
# constrain that courses only have 'chapter' children
err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter")
err_cnt += validate_category_hierarchy(module_store, course_id, "course", "chapter")
# constrain that chapters only have 'sequentials'
err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential")
err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential")
# constrain that sequentials only have 'verticals'
err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical")
err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical")
# check for a presence of a course marketing video
location_elements = course_id.split('/')
if Location(['i4x', location_elements[0], location_elements[1], 'about', 'video', None]) not in module_store.modules[course_id]:
print "WARN: Missing course marketing video. It is recommended that every course have a marketing video."
warn_cnt += 1
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
......
......@@ -83,10 +83,11 @@ var CohortManager = (function ($) {
cohort_id = el.data('id');
state = state_detail;
render();
return false;
}
function add_to_cohorts_list(item) {
var li = $('<li><a></a></li>');
var li = $('<li><a href="#"></a></li>');
$("a", li).text(item.name)
.data('href', url + '/' + item.id)
.addClass('link')
......
......@@ -209,6 +209,14 @@ $.widget("ui.draggable", $.ui.mouse, {
// computation of pageY or scrollTop() or caching of scrollTop at same state as pageY
// btw: known bug in jqueryui http://bugs.jqueryui.com/ticket/5718
if (this.scrollParent.is(document) && this.cssPosition === 'relative') {
// need to catch the original parent having been shoved down during drag (perhaps by
// events)
// update cached originals if it shifted
if (this.offset && this.offset.parent && this.offset.parent.top !== this._getParentOffset().top) {
var deltaY = this.offset.parent.top - this._getParentOffset().top;
this.offset.parent.top = this._getParentOffset().top;
this.originalPageY -= deltaY;
}
this.helper[0].style.top = (event.pageY - this.originalPageY) +"px";
}
else this.helper[0].style.top = this.position.top+"px";
......
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
from certificates.models import CertificateWhitelist
from django.contrib.auth.models import User
class Command(BaseCommand):
help = """
Sets or gets the certificate whitelist for a given
user/course
Add a user to the whitelist for a course
$ ... cert_whitelist --add joe -c "MITx/6.002x/2012_Fall"
Remove a user from the whitelist for a course
$ ... cert_whitelist --del joe -c "MITx/6.002x/2012_Fall"
Print out who is whitelisted for a course
$ ... cert_whitelist -c "MITx/6.002x/2012_Fall"
"""
option_list = BaseCommand.option_list + (
make_option('-a', '--add',
metavar='USER',
dest='add',
default=False,
help='user to add to the certificate whitelist'),
make_option('-d', '--del',
metavar='USER',
dest='del',
default=False,
help='user to remove from the certificate whitelist'),
make_option('-c', '--course-id',
metavar='COURSE_ID',
dest='course_id',
default=False,
help="course id to query"),
)
def handle(self, *args, **options):
course_id = options['course_id']
if not course_id:
raise CommandError("You must specify a course-id")
if options['add'] and options['del']:
raise CommandError("Either remove or add a user, not both")
if options['add'] or options['del']:
user_str = options['add'] or options['del']
if '@' in user_str:
user = User.objects.get(email=user_str)
else:
user = User.objects.get(username=user_str)
cert_whitelist, created = \
CertificateWhitelist.objects.get_or_create(
user=user, course_id=course_id)
if options['add']:
cert_whitelist.whitelist = True
elif options['del']:
cert_whitelist.whitelist = False
cert_whitelist.save()
whitelist = CertificateWhitelist.objects.all()
print "User whitelist for course {0}:\n{1}".format(course_id,
'\n'.join(["{0} {1} {2}".format(
u.user.username, u.user.email, u.whitelist)
for u in whitelist]))
# -*- 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 model 'CertificateWhitelist'
db.create_table('certificates_certificatewhitelist', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)),
('whitelist', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('certificates', ['CertificateWhitelist'])
def backwards(self, orm):
# Deleting model 'CertificateWhitelist'
db.delete_table('certificates_certificatewhitelist')
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'}),
'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']
\ No newline at end of file
......@@ -35,6 +35,19 @@ State diagram:
v v v
[downloadable] [downloadable] [deleted]
Eligibility:
Students are eligible for a certificate if they pass the course
with the following exceptions:
If the student has allow_certificate set to False in the student profile
he will never be issued a certificate.
If the user and course is present in the certificate whitelist table
then the student will be issued a certificate regardless of his grade,
unless he has allow_certificate set to False.
"""
......@@ -46,8 +59,20 @@ class CertificateStatuses(object):
deleted = 'deleted'
downloadable = 'downloadable'
notpassing = 'notpassing'
restricted = 'restricted'
error = 'error'
class CertificateWhitelist(models.Model):
"""
Tracks students who are whitelisted, all users
in this table will always qualify for a certificate
regardless of their grade unless they are on the
embargoed country restriction list
(allow_certificate set to False in userprofile).
"""
user = models.ForeignKey(User)
course_id = models.CharField(max_length=255, blank=True, default='')
whitelist = models.BooleanField(default=0)
class GeneratedCertificate(models.Model):
user = models.ForeignKey(User)
......@@ -87,6 +112,10 @@ def certificate_status_for_student(student, course_id):
deleted - The certificate has been deleted.
downloadable - The certificate is available for download.
notpassing - The student was graded but is not passing
restricted - The student is on the restricted embargo list and
should not be issued a certificate. This will
be set if allow_certificate is set to False in
the userprofile table
If the status is "downloadable", the dictionary also contains
"download_url".
......
from certificates.models import GeneratedCertificate
from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status
from certificates.models import CertificateWhitelist
from courseware import grades, courses
from django.test.client import RequestFactory
......@@ -71,6 +72,8 @@ class XQueueCertInterface(object):
settings.XQUEUE_INTERFACE['django_auth'],
requests_auth,
)
self.whitelist = CertificateWhitelist.objects.all()
self.restricted = UserProfile.objects.filter(allow_certificate=False)
def regen_cert(self, student, course_id):
"""
......@@ -93,49 +96,7 @@ class XQueueCertInterface(object):
"""
VALID_STATUSES = [status.error, status.downloadable]
cert_status = certificate_status_for_student(
student, course_id)['status']
if cert_status in VALID_STATUSES:
# grade the student
course = courses.get_course_by_id(course_id)
grade = grades.grade(student, self.request, course)
profile = UserProfile.objects.get(user=student)
try:
cert = GeneratedCertificate.objects.get(
user=student, course_id=course_id)
except GeneratedCertificate.DoesNotExist:
logger.critical("Attempting to regenerate a certificate"
"for a user that doesn't have one")
raise
if grade['grade'] is not None:
cert.status = status.regenerating
cert.name = profile.name
contents = {
'action': 'regen',
'delete_verify_uuid': cert.verify_uuid,
'delete_download_uuid': cert.download_uuid,
'username': cert.user.username,
'course_id': cert.course_id,
'name': profile.name,
}
key = cert.key
self._send_to_xqueue(contents, key)
cert.save()
else:
cert.status = status.notpassing
cert.name = profile.name
cert.save()
return cert_status
raise NotImplementedError
def del_cert(self, student, course_id):
......@@ -152,34 +113,7 @@ class XQueueCertInterface(object):
"""
VALID_STATUSES = [status.error, status.downloadable]
cert_status = certificate_status_for_student(
student, course_id)['status']
if cert_status in VALID_STATUSES:
try:
cert = GeneratedCertificate.objects.get(
user=student, course_id=course_id)
except GeneratedCertificate.DoesNotExist:
logger.warning("Attempting to delete a certificate"
"for a user that doesn't have one")
raise
cert.status = status.deleting
contents = {
'action': 'delete',
'delete_verify_uuid': cert.verify_uuid,
'delete_download_uuid': cert.download_uuid,
'username': cert.user.username,
}
key = cert.key
self._send_to_xqueue(contents, key)
cert.save()
return cert_status
raise NotImplementedError
def add_cert(self, student, course_id):
"""
......@@ -189,13 +123,17 @@ class XQueueCertInterface(object):
course_id - courseenrollment.course_id (string)
Request a new certificate for a student.
Will change the certificate status to 'deleting'.
Will change the certificate status to 'generating'.
Certificate must be in the 'unavailable', 'error',
'deleted' or 'generating' state.
If a student has a passing grade a request will made
for a new cert
If a student has a passing grade or is in the whitelist
table for the course a request will made for a new cert.
If a student has allow_certificate set to False in the
userprofile table the status will change to 'restricted'
If a student does not have a passing grade the status
will change to status.notpassing
......@@ -214,30 +152,41 @@ class XQueueCertInterface(object):
if cert_status in VALID_STATUSES:
# grade the student
course = courses.get_course_by_id(course_id)
grade = grades.grade(student, self.request, course)
profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
if grade['grade'] is not None:
cert_status = status.generating
grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists()
if is_whitelisted or grade['grade'] is not None:
key = make_hashkey(random.random())
cert.status = cert_status
cert.grade = grade['percent']
cert.user = student
cert.course_id = course_id
cert.key = key
cert.name = profile.name
contents = {
'action': 'create',
'username': student.username,
'course_id': course_id,
'name': profile.name,
}
self._send_to_xqueue(contents, key)
# 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
else:
contents = {
'action': 'create',
'username': student.username,
'course_id': course_id,
'name': profile.name,
}
cert.status = status.generating
self._send_to_xqueue(contents, key)
cert.save()
else:
cert_status = status.notpassing
......
......@@ -61,7 +61,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
#if the course-user is cohorted, then add the group id
group_id = get_cohort_id(user,course_id)
group_id = get_cohort_id(user, course_id)
if group_id:
default_query_params["group_id"] = group_id
......
......@@ -273,12 +273,16 @@
% 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'):
% 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':
Grade required for a certificate: <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% elif cert_status['status'] == 'restricted':
<p class="message-copy">
Your certificate is being held while we seek confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting <a class="contact-link" href="mailto:info@edx.org">info@edx.org</a>.
</p>
% endif
</p>
% endif
......
......@@ -84,7 +84,7 @@
<nav class="categories">
<a href="#organization">Organization</a>
<a href="#students">Students</a>
<a href="${reverse('help_edx')}">Students</a>
<a href="#technology-platform">Technology Platform</a>
</nav>
</section>
......
......@@ -51,6 +51,40 @@
</div>
</article>
<article id="associate-legal-counsel" class="job">
<div class="inner-wrapper">
<h3><strong>ASSOCIATE LEGAL COUNSEL</strong></h3>
<p>We are seeking a talented lawyer with the ability to operate independently in a fast-paced environment and work proactively with all members of the edX team. You must have thorough knowledge of intellectual property law, contracts and licensing. </p>
<p><strong>Key Responsibilities: </strong></p>
<ul>
<li>Drive the negotiating, reviewing, drafting and overseeing of a wide range of transactional arrangements, including collaborations related to the provision of online education, inbound and outbound licensing of intellectual property, strategic partnerships, nondisclosure agreements, and services agreements.</li>
<li>Provide counseling on the legal implications/considerations of business and technical strategies and projects, with special emphasis on regulations related to higher education, data security and privacy.</li>
<li>Provide advice and support company-wide on a variety of legal issues in a timely and effective manner.</li>
<li>Assist on other matters as needed.</li>
</ul>
<p><strong>Requirements:</strong></p>
<li>JD from an accredited law school</li>
<li>Massachusetts bar admission required</li>
<li>2-3 years of transactional experience at a major law firm and/or as an in-house counselor</li>
<li>Substantial IP licensing experience</li>
<li>Knowledge of copyright, trademark and patent law</li>
<li>Experience with open source content and open source software preferred</li>
<li>Outstanding communications skills (written and oral)</li>
<li>Experience with drafting and legal review of internet privacy policies and terms of use.</li>
<li>Understanding of how to balance legal risks with business objectives</li>
<li>Ability to develop an innovative approach to legal issues in support of strategic business initiatives</li>
<li>An internal business and customer focused proactive attitude with ability to prioritize effectively</li>
<li>Experience with higher education preferred but not required</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="instructional-designer" class="job">
<div class="inner-wrapper">
<h3><strong>INSTRUCTIONAL DESIGNER</strong> &mdash; CONTRACT OPPORTUNITY</h3>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment