Commit 6847779f by Victor Shnayder

Merge remote-tracking branch 'origin/master' into feature/victor/cohorts

Conflicts:
	common/djangoapps/student/models.py
parents 87e92644 d237e68a
1.8.7-p371
source :rubygems source :rubygems
ruby "1.9.3" ruby "1.8.7"
gem 'rake' gem 'rake', '~> 10.0.3'
gem 'sass', '3.1.15' gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6' gem 'bourbon', '~> 1.3.6'
gem 'colorize' gem 'colorize', '~> 0.5.8'
gem 'launchy' gem 'launchy', '~> 2.1.2'
...@@ -229,7 +229,7 @@ PIPELINE_JS = { ...@@ -229,7 +229,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee')
) + ['js/base.js'], ) + [ 'js/hesitate.js', 'js/base.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
}, },
'module-js': { 'module-js': {
......
...@@ -55,13 +55,6 @@ $(document).ready(function() { ...@@ -55,13 +55,6 @@ $(document).ready(function() {
$("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput); $("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput);
$('.sync-date, .remove-date').bind('click', autosaveInput); $('.sync-date, .remove-date').bind('click', autosaveInput);
// making the unit list sortable
$('.sortable-unit-list').sortable({
axis: 'y',
handle: '.drag-handle',
update: onUnitReordered
});
// expand/collapse methods for optional date setters // expand/collapse methods for optional date setters
$('.set-date').bind('click', showDateSetter); $('.set-date').bind('click', showDateSetter);
$('.remove-date').bind('click', removeDateSetter); $('.remove-date').bind('click', removeDateSetter);
...@@ -87,18 +80,56 @@ $(document).ready(function() { ...@@ -87,18 +80,56 @@ $(document).ready(function() {
$('.import .file-input').click(); $('.import .file-input').click();
}); });
// making the unit list draggable. Note: sortable didn't work b/c it considered
// drop points which the user hovered over as destinations and proactively changed
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
// point was the last dom change.
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
stack: '.unit',
revert: "invalid"
});
// Subsection reordering // Subsection reordering
$('.subsection-list > ol').sortable({ $('.id-holder').draggable({
axis: 'y', axis: 'y',
handle: '.section-item .drag-handle', handle: '.section-item .drag-handle',
update: onSubsectionReordered stack: '.id-holder',
revert: "invalid"
}); });
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
handle: 'header .drag-handle',
stack: '.courseware-section',
revert: "invalid"
});
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
tolerance: "pointer",
hoverClass: "dropover",
drop: onUnitReordered
});
$('.subsection-list > ol').droppable({
// why don't we have a more useful class for subsections than id-holder?
accept : '.id-holder', // '.unit, .id-holder',
tolerance: "pointer",
hoverClass: "dropover",
drop: onSubsectionReordered,
greedy: true
});
// Section reordering // Section reordering
$('.courseware-overview').sortable({ $('.courseware-overview').droppable({
axis: 'y', accept : '.courseware-section',
handle: 'header .drag-handle', tolerance: "pointer",
update: onSectionReordered drop: onSectionReordered,
greedy: true
}); });
$('.new-course-button').bind('click', addNewCourse); $('.new-course-button').bind('click', addNewCourse);
...@@ -240,54 +271,76 @@ function removePolicyMetadata(e) { ...@@ -240,54 +271,76 @@ function removePolicyMetadata(e) {
saveSubsection() saveSubsection()
} }
function expandSection(event) {
// This method only changes the ordering of the child objects in a subsection $(event.delegateTarget).removeClass('collapsed');
function onUnitReordered() { $(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse');
var subsection_id = $(this).data('subsection-id'); }
var _els = $(this).children('li:.leaf'); function onUnitReordered(event, ui) {
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // a unit's been dropped on this subsection,
// figure out where it came from and where it slots in.
// call into server to commit the new order _handleReorder(event, ui, 'subsection-id', 'li:.leaf');
$.ajax({ }
url: "/save_item",
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
function onSectionReordered(event, ui) {
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
_handleReorder(event, ui, 'course-id', '.courseware-section');
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// if new to this parent, figure out which parent to remove it from and do so
if (!_.contains(children, ui.draggable.data('id'))) {
var old_parent = ui.draggable.parent();
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
old_children = _.without(old_children, ui.draggable.data('id'));
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
});
}
else {
// staying in same parent
// remove so that the replacement in the right place doesn't double it
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id'));
break;
}
}
// see if it goes at end (the above loop didn't insert it)
if (!_.contains(children, ui.draggable.data('id'))) {
$(event.target).append(ui.draggable);
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
children.push(ui.draggable.data('id'));
}
$.ajax({
url: "/save_item",
type: "POST", type: "POST",
dataType: "json", dataType: "json",
contentType: "application/json", contentType: "application/json",
data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
}); });
}
function onSubsectionReordered() {
var section_id = $(this).data('section-id');
var _els = $(this).children('li:.branch');
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : section_id, 'children' : children})
});
}
function onSectionReordered() {
var course_id = $(this).data('course-id');
var _els = $(this).children('section:.branch');
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// call into server to commit the new order
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : course_id, 'children' : children})
});
} }
function getEdxTimeFromDateTimeVals(date_val, time_val, format) { function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
......
/*
* Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
* did not occur on the event.currentTarget.
*
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
*
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/
CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) {
this.executeOnTimeOut = executeOnTimeOut;
this.cancelSelector = cancelSelector;
this.timeoutEventId = null;
this.originalEvent = null;
this.onlyOnce = (onlyOnce === true);
}
CMS.HesitateEvent.DURATION = 400;
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);
}
}
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);
}
CMS.HesitateEvent.prototype.untrigger = function(event) {
console.log('untrigger');
if (this.timeoutEventId) {
window.clearTimeout(this.timeoutEventId);
$(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger);
}
this.timeoutEventId = null;
}
\ No newline at end of file
...@@ -25,18 +25,18 @@ input.courseware-unit-search-input { ...@@ -25,18 +25,18 @@ input.courseware-unit-search-input {
width: 145px; width: 145px;
.status-label { .status-label {
position: absolute; position: absolute;
top: 2px; top: 2px;
right: -5px; right: -5px;
display: none; display: none;
width: 110px; width: 110px;
padding: 5px 40px 5px 10px; padding: 5px 40px 5px 10px;
@include border-radius(3px); @include border-radius(3px);
color: $lightGrey; color: $lightGrey;
text-align: right; text-align: right;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
line-height: 16px; line-height: 16px;
} }
.menu-toggle { .menu-toggle {
...@@ -643,4 +643,21 @@ input.courseware-unit-search-input { ...@@ -643,4 +643,21 @@ input.courseware-unit-search-input {
margin-top: 10px; margin-top: 10px;
font-size: 13px; font-size: 13px;
color: $darkGrey; color: $darkGrey;
} }
\ No newline at end of file
// sort/drag and drop
.ui-droppable {
min-height: 20px;
&.dropover {
padding-top: 10px;
padding-bottom: 10px;
}
}
ol.ui-droppable .branch:first-child .section-item {
border-top: none;
}
import csv import csv
import os
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
from os.path import isdir
from optparse import make_option from optparse import make_option
from django.core.management.base import BaseCommand from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser from student.models import TestCenterUser
class Command(BaseCommand): class Command(BaseCommand):
CSV_TO_MODEL_FIELDS = OrderedDict([ CSV_TO_MODEL_FIELDS = OrderedDict([
# Skipping optional field CandidateID # Skipping optional field CandidateID
("ClientCandidateID", "client_candidate_id"), ("ClientCandidateID", "client_candidate_id"),
...@@ -34,43 +36,52 @@ class Command(BaseCommand): ...@@ -34,43 +36,52 @@ class Command(BaseCommand):
("FAXCountryCode", "fax_country_code"), ("FAXCountryCode", "fax_country_code"),
("CompanyName", "company_name"), ("CompanyName", "company_name"),
# Skipping optional field CustomQuestion # Skipping optional field CustomQuestion
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
]) ])
# define defaults, even thought 'store_true' shouldn't need them.
# (call_command will set None as default value for all options that don't have one,
# so one cannot rely on presence/absence of flags in that world.)
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
make_option( make_option('--dest-from-settings',
'--dump_all', action='store_true',
action='store_true', dest='dest-from-settings',
dest='dump_all', default=False,
), help='Retrieve the destination to export to from django.'),
make_option('--destination',
action='store',
dest='destination',
default=None,
help='Where to store the exported files')
) )
args = '<output_file_or_dir>'
help = """
Export user demographic information from TestCenterUser model into a tab delimited
text file with a format that Pearson expects.
"""
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
def handle(self, **options):
# update time should use UTC in order to be comparable to the user_updated_at # update time should use UTC in order to be comparable to the user_updated_at
# field # field
uploaded_at = datetime.utcnow() uploaded_at = datetime.utcnow()
# if specified destination is an existing directory, then # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist, # create a filename for it automatically. If it doesn't exist,
# or exists as a file, then we will just write to it. # then we will create the directory.
# Name will use timestamp -- this is UTC, so it will look funny, # Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps # but it should at least be consistent with the other timestamps
# used in the system. # used in the system.
dest = args[0] if 'dest-from-settings' in options and options['dest-from-settings']:
if isdir(dest): if 'LOCAL_EXPORT' in settings.PEARSON:
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat")) dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
elif 'destination' in options and options['destination']:
dest = options['destination']
else: else:
destfile = dest raise CommandError('--destination or --dest-from-settings must be used')
if not os.path.isdir(dest):
os.makedirs(dest)
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
# strings must be in latin-1 format. CSV parser will # strings must be in latin-1 format. CSV parser will
# otherwise convert unicode objects to ascii. # otherwise convert unicode objects to ascii.
def ensure_encoding(value): def ensure_encoding(value):
...@@ -78,8 +89,8 @@ class Command(BaseCommand): ...@@ -78,8 +89,8 @@ class Command(BaseCommand):
return value.encode('iso-8859-1') return value.encode('iso-8859-1')
else: else:
return value return value
dump_all = kwargs['dump_all'] # dump_all = options['dump_all']
with open(destfile, "wb") as outfile: with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile, writer = csv.DictWriter(outfile,
...@@ -89,7 +100,7 @@ class Command(BaseCommand): ...@@ -89,7 +100,7 @@ class Command(BaseCommand):
extrasaction='ignore') extrasaction='ignore')
writer.writeheader() writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'): for tcu in TestCenterUser.objects.order_by('id'):
if dump_all or tcu.needs_uploading: if tcu.needs_uploading: # or dump_all
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field))) record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
for csv_field, model_field for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items()) in Command.CSV_TO_MODEL_FIELDS.items())
...@@ -97,6 +108,3 @@ class Command(BaseCommand): ...@@ -97,6 +108,3 @@ class Command(BaseCommand):
writer.writerow(record) writer.writerow(record)
tcu.uploaded_at = uploaded_at tcu.uploaded_at = uploaded_at
tcu.save() tcu.save()
import csv import csv
import os
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
from os.path import isdir, join
from optparse import make_option from optparse import make_option
from django.core.management.base import BaseCommand from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterRegistration, ACCOMMODATION_REJECTED_CODE
from student.models import TestCenterRegistration
class Command(BaseCommand): class Command(BaseCommand):
CSV_TO_MODEL_FIELDS = OrderedDict([ CSV_TO_MODEL_FIELDS = OrderedDict([
('AuthorizationTransactionType', 'authorization_transaction_type'), ('AuthorizationTransactionType', 'authorization_transaction_type'),
('AuthorizationID', 'authorization_id'), ('AuthorizationID', 'authorization_id'),
...@@ -20,51 +22,60 @@ class Command(BaseCommand): ...@@ -20,51 +22,60 @@ class Command(BaseCommand):
('Accommodations', 'accommodation_code'), ('Accommodations', 'accommodation_code'),
('EligibilityApptDateFirst', 'eligibility_appointment_date_first'), ('EligibilityApptDateFirst', 'eligibility_appointment_date_first'),
('EligibilityApptDateLast', 'eligibility_appointment_date_last'), ('EligibilityApptDateLast', 'eligibility_appointment_date_last'),
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
]) ])
args = '<output_file_or_dir>'
help = """
Export user registration information from TestCenterRegistration model into a tab delimited
text file with a format that Pearson expects.
"""
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
make_option( make_option('--dest-from-settings',
'--dump_all', action='store_true',
action='store_true', dest='dest-from-settings',
dest='dump_all', default=False,
), help='Retrieve the destination to export to from django.'),
make_option( make_option('--destination',
'--force_add', action='store',
action='store_true', dest='destination',
dest='force_add', default=None,
), help='Where to store the exported files'),
make_option('--dump_all',
action='store_true',
dest='dump_all',
default=False,
),
make_option('--force_add',
action='store_true',
dest='force_add',
default=False,
),
) )
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
# update time should use UTC in order to be comparable to the user_updated_at def handle(self, **options):
# update time should use UTC in order to be comparable to the user_updated_at
# field # field
uploaded_at = datetime.utcnow() uploaded_at = datetime.utcnow()
# if specified destination is an existing directory, then # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist, # create a filename for it automatically. If it doesn't exist,
# or exists as a file, then we will just write to it. # then we will create the directory.
# Name will use timestamp -- this is UTC, so it will look funny, # Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps # but it should at least be consistent with the other timestamps
# used in the system. # used in the system.
dest = args[0] if 'dest-from-settings' in options and options['dest-from-settings']:
if isdir(dest): if 'LOCAL_EXPORT' in settings.PEARSON:
destfile = join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat")) dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
elif 'destination' in options and options['destination']:
dest = options['destination']
else: else:
destfile = dest raise CommandError('--destination or --dest-from-settings must be used')
if not os.path.isdir(dest):
os.makedirs(dest)
dump_all = kwargs['dump_all'] destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
dump_all = options['dump_all']
with open(destfile, "wb") as outfile: with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile, writer = csv.DictWriter(outfile,
...@@ -81,13 +92,11 @@ class Command(BaseCommand): ...@@ -81,13 +92,11 @@ class Command(BaseCommand):
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S") record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d") record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d")
record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d") record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d")
if kwargs['force_add']: if record["Accommodations"] == ACCOMMODATION_REJECTED_CODE:
record["Accommodations"] = ""
if options['force_add']:
record['AuthorizationTransactionType'] = 'Add' record['AuthorizationTransactionType'] = 'Add'
writer.writerow(record) writer.writerow(record)
tcr.uploaded_at = uploaded_at tcr.uploaded_at = uploaded_at
tcr.save() tcr.save()
import csv
from zipfile import ZipFile, is_zipfile
from time import strptime, strftime
from collections import OrderedDict
from datetime import datetime
from os.path import isdir
from optparse import make_option
from dogapi import dog_http_api, dog_stats_api
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from student.models import TestCenterUser, TestCenterRegistration
class Command(BaseCommand):
dog_http_api.api_key = settings.DATADOG_API
args = '<input zip file>'
help = """
Import Pearson confirmation files and update TestCenterUser
and TestCenterRegistration tables with status.
"""
@staticmethod
def datadog_error(string, tags):
dog_http_api.event("Pearson Import", string, alert_type='error', tags=[tags])
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
source_zip = args[0]
if not is_zipfile(source_zip):
error = "Input file is not a zipfile: \"{}\"".format(source_zip)
Command.datadog_error(error, source_zip)
raise CommandError(error)
# loop through all files in zip, and process them based on filename prefix:
with ZipFile(source_zip, 'r') as zipfile:
for fileinfo in zipfile.infolist():
with zipfile.open(fileinfo) as zipentry:
if fileinfo.filename.startswith("eac-"):
self.process_eac(zipentry)
elif fileinfo.filename.startswith("vcdc-"):
self.process_vcdc(zipentry)
else:
error = "Unrecognized confirmation file type\"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile)
Command.datadog_error(error, source_zip)
raise CommandError(error)
def process_eac(self, eacfile):
print "processing eac"
reader = csv.DictReader(eacfile, delimiter="\t")
for row in reader:
client_authorization_id = row['ClientAuthorizationID']
if not client_authorization_id:
if row['Status'] == 'Error':
Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']), eacfile.name)
else:
Command.datadog_error("Encountered bad record: {}".format(row), eacfile.name)
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)
# now update the record:
registration.upload_status = row['Status']
registration.upload_error_message = row['Message']
try:
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
# store the authorization Id if one is provided. (For debugging)
if row['AuthorizationID']:
try:
registration.authorization_id = int(row['AuthorizationID'])
except ValueError as ve:
Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
registration.confirmed_at = datetime.utcnow()
registration.save()
except TestCenterRegistration.DoesNotExist:
Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile.name)
def process_vcdc(self, vcdcfile):
print "processing vcdc"
reader = csv.DictReader(vcdcfile, delimiter="\t")
for row in reader:
client_candidate_id = row['ClientCandidateID']
if not client_candidate_id:
if row['Status'] == 'Error':
Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']), vcdcfile.name)
else:
Command.datadog_error("Encountered bad record: {}".format(row), vcdcfile.name)
else:
try:
tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username), vcdcfile.name)
# now update the record:
tcuser.upload_status = row['Status']
tcuser.upload_error_message = row['Message']
try:
tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
# store the candidate Id if one is provided. (For debugging)
if row['CandidateID']:
try:
tcuser.candidate_id = int(row['CandidateID'])
except ValueError as ve:
Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
tcuser.confirmed_at = datetime.utcnow()
tcuser.save()
except TestCenterUser.DoesNotExist:
Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name)
...@@ -71,6 +71,12 @@ class Command(BaseCommand): ...@@ -71,6 +71,12 @@ class Command(BaseCommand):
dest='ignore_registration_dates', dest='ignore_registration_dates',
help='find exam info for course based on exam_series_code, even if the exam is not active.' help='find exam info for course based on exam_series_code, even if the exam is not active.'
), ),
make_option(
'--create_dummy_exam',
action='store_true',
dest='create_dummy_exam',
help='create dummy exam info for course, even if course exists'
),
) )
args = "<student_username course_id>" args = "<student_username course_id>"
help = "Create or modify a TestCenterRegistration entry for a given Student" help = "Create or modify a TestCenterRegistration entry for a given Student"
...@@ -98,15 +104,20 @@ class Command(BaseCommand): ...@@ -98,15 +104,20 @@ class Command(BaseCommand):
except TestCenterUser.DoesNotExist: except TestCenterUser.DoesNotExist:
raise CommandError("User \"{}\" does not have an existing demographics record".format(username)) raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
# check to see if a course_id was specified, and use information from that: # get an "exam" object. Check to see if a course_id was specified, and use information from that:
try: exam = None
course = course_from_id(course_id) create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
if 'ignore_registration_dates' in our_options: if not create_dummy_exam:
examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')] try:
exam = examlist[0] if len(examlist) > 0 else None course = course_from_id(course_id)
else: if 'ignore_registration_dates' in our_options:
exam = course.current_test_center_exam examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')]
except ItemNotFoundError: exam = examlist[0] if len(examlist) > 0 else None
else:
exam = course.current_test_center_exam
except ItemNotFoundError:
pass
else:
# otherwise use explicit values (so we don't have to define a course): # otherwise use explicit values (so we don't have to define a course):
exam_name = "Dummy Placeholder Name" exam_name = "Dummy Placeholder Name"
exam_info = { 'Exam_Series_Code': our_options['exam_series_code'], exam_info = { 'Exam_Series_Code': our_options['exam_series_code'],
...@@ -120,7 +131,7 @@ class Command(BaseCommand): ...@@ -120,7 +131,7 @@ class Command(BaseCommand):
our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date) our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
if exam is None: if exam is None:
raise CommandError("Exam for course_id {%s} does not exist".format(course_id)) raise CommandError("Exam for course_id {} does not exist".format(course_id))
exam_code = exam.exam_series_code exam_code = exam.exam_series_code
......
from optparse import make_option from optparse import make_option
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser, TestCenterUserForm from student.models import TestCenterUser, TestCenterUserForm
...@@ -161,15 +161,16 @@ class Command(BaseCommand): ...@@ -161,15 +161,16 @@ class Command(BaseCommand):
if form.is_valid(): if form.is_valid():
form.update_and_save() form.update_and_save()
else: else:
errorlist = []
if (len(form.errors) > 0): if (len(form.errors) > 0):
print "Field Form errors encountered:" errorlist.append("Field Form errors encountered:")
for fielderror in form.errors: for fielderror in form.errors:
print "Field Form Error: %s" % fielderror errorlist.append("Field Form Error: {}".format(fielderror))
if (len(form.non_field_errors()) > 0): if (len(form.non_field_errors()) > 0):
print "Non-field Form errors encountered:" errorlist.append("Non-field Form errors encountered:")
for nonfielderror in form.non_field_errors: for nonfielderror in form.non_field_errors:
print "Non-field Form Error: %s" % nonfielderror errorlist.append("Non-field Form Error: {}".format(nonfielderror))
raise CommandError("\n".join(errorlist))
else: else:
print "No changes necessary to make to existing user's demographics." print "No changes necessary to make to existing user's demographics."
......
import os
from optparse import make_option
from stat import S_ISDIR
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
from dogapi import dog_http_api, dog_stats_api
import paramiko
import boto
dog_http_api.api_key = settings.DATADOG_API
class Command(BaseCommand):
help = """
This command handles the importing and exporting of student records for
Pearson. It uses some other Django commands to export and import the
files and then uploads over SFTP to Pearson and stuffs the entry in an
S3 bucket for archive purposes.
Usage: django-admin.py pearson-transfer --mode [import|export|both]
"""
option_list = BaseCommand.option_list + (
make_option('--mode',
action='store',
dest='mode',
default='both',
choices=('import', 'export', 'both'),
help='mode is import, export, or both'),
)
def handle(self, **options):
if not hasattr(settings, 'PEARSON'):
raise CommandError('No PEARSON entries in auth/env.json.')
# check settings needed for either import or export:
for value in ['SFTP_HOSTNAME', 'SFTP_USERNAME', 'SFTP_PASSWORD', 'S3_BUCKET']:
if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value))
for value in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']:
if not hasattr(settings, value):
raise CommandError('No entry in the AWS settings'
'(env/auth.json) for {0}'.format(value))
# check additional required settings for import and export:
if options['mode'] in ('export', 'both'):
for value in ['LOCAL_EXPORT','SFTP_EXPORT']:
if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value))
# make sure that the import directory exists or can be created:
source_dir = settings.PEARSON['LOCAL_EXPORT']
if not os.path.isdir(source_dir):
os.makedirs(source_dir)
if options['mode'] in ('import', 'both'):
for value in ['LOCAL_IMPORT','SFTP_IMPORT']:
if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value))
# make sure that the import directory exists or can be created:
dest_dir = settings.PEARSON['LOCAL_IMPORT']
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
def sftp(files_from, files_to, mode, deleteAfterCopy=False):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
try:
t = paramiko.Transport((settings.PEARSON['SFTP_HOSTNAME'], 22))
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
password=settings.PEARSON['SFTP_PASSWORD'])
sftp = paramiko.SFTPClient.from_transport(t)
if mode == 'export':
try:
sftp.chdir(files_to)
except IOError:
raise CommandError('SFTP destination path does not exist: {}'.format(files_to))
for filename in os.listdir(files_from):
sftp.put(files_from + '/' + filename, filename)
if deleteAfterCopy:
os.remove(os.path.join(files_from, filename))
else:
try:
sftp.chdir(files_from)
except IOError:
raise CommandError('SFTP source path does not exist: {}'.format(files_from))
for filename in sftp.listdir('.'):
# skip subdirectories
if not S_ISDIR(sftp.stat(filename).st_mode):
sftp.get(filename, files_to + '/' + filename)
# delete files from sftp server once they are successfully pulled off:
if deleteAfterCopy:
sftp.remove(filename)
except:
dog_http_api.event('pearson {0}'.format(mode),
'sftp uploading failed',
alert_type='error')
raise
finally:
sftp.close()
t.close()
def s3(files_from, bucket, mode, deleteAfterCopy=False):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
try:
for filename in os.listdir(files_from):
source_file = os.path.join(files_from, filename)
# use mode as name of directory into which to write files
dest_file = os.path.join(mode, filename)
upload_file_to_s3(bucket, source_file, dest_file)
if deleteAfterCopy:
os.remove(files_from + '/' + filename)
except:
dog_http_api.event('pearson {0}'.format(mode),
's3 archiving failed')
raise
def upload_file_to_s3(bucket, source_file, dest_file):
"""
Upload file to S3
"""
s3 = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
settings.AWS_SECRET_ACCESS_KEY)
from boto.s3.key import Key
b = s3.get_bucket(bucket)
k = Key(b)
k.key = "{filename}".format(filename=dest_file)
k.set_contents_from_filename(source_file)
def export_pearson():
options = { 'dest-from-settings' : True }
call_command('pearson_export_cdd', **options)
call_command('pearson_export_ead', **options)
mode = 'export'
sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy = False)
s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True)
def import_pearson():
mode = 'import'
try:
sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy = True)
s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False)
except Exception as e:
dog_http_api.event('Pearson Import failure', str(e))
raise e
else:
for filename in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
filepath = os.path.join(settings.PEARSON['LOCAL_IMPORT'], filename)
call_command('pearson_import_conf_zip', filepath)
os.remove(filepath)
# actually do the work!
if options['mode'] in ('export', 'both'):
export_pearson()
if options['mode'] in ('import', 'both'):
import_pearson()
...@@ -402,7 +402,11 @@ class TestCenterRegistration(models.Model): ...@@ -402,7 +402,11 @@ class TestCenterRegistration(models.Model):
def exam_authorization_count(self): def exam_authorization_count(self):
# TODO: figure out if this should really go in the database (with a default value). # TODO: figure out if this should really go in the database (with a default value).
return 1 return 1
@property
def needs_uploading(self):
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
@classmethod @classmethod
def create(cls, testcenter_user, exam, accommodation_request): def create(cls, testcenter_user, exam, accommodation_request):
registration = cls(testcenter_user = testcenter_user) registration = cls(testcenter_user = testcenter_user)
...@@ -525,6 +529,10 @@ def get_testcenter_registration(user, course_id, exam_series_code): ...@@ -525,6 +529,10 @@ def get_testcenter_registration(user, course_id, exam_series_code):
return [] return []
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code) return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=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): def unique_id_for_user(user):
""" """
Return a unique id for a user, suitable for inserting into Return a unique id for a user, suitable for inserting into
......
...@@ -116,9 +116,11 @@ class CapaModule(XModule): ...@@ -116,9 +116,11 @@ class CapaModule(XModule):
self.grace_period = None self.grace_period = None
self.close_date = self.display_due_date self.close_date = self.display_due_date
self.max_attempts = self.metadata.get('attempts', None) max_attempts = self.metadata.get('attempts', None)
if self.max_attempts is not None: if max_attempts:
self.max_attempts = int(self.max_attempts) self.max_attempts = int(max_attempts)
else:
self.max_attempts = None
self.show_answer = self.metadata.get('showanswer', 'closed') self.show_answer = self.metadata.get('showanswer', 'closed')
......
...@@ -690,7 +690,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -690,7 +690,7 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def registration_end_date_text(self): def registration_end_date_text(self):
return time.strftime("%b %d, %Y", self.registration_end_date) return time.strftime("%b %d, %Y at %H:%M UTC", self.registration_end_date)
@property @property
def current_test_center_exam(self): def current_test_center_exam(self):
......
...@@ -8,13 +8,13 @@ define('ElOutput', ['logme'], function (logme) { ...@@ -8,13 +8,13 @@ define('ElOutput', ['logme'], function (logme) {
function ElOutput(config, state) { function ElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) { if ($.isPlainObject(config.functions["function"])) {
processFuncObj(config.functions.function); processFuncObj(config.functions["function"]);
} else if ($.isArray(config.functions.function)) { } else if ($.isArray(config.functions["function"])) {
(function (c1) { (function (c1) {
while (c1 < config.functions.function.length) { while (c1 < config.functions["function"].length) {
if ($.isPlainObject(config.functions.function[c1])) { if ($.isPlainObject(config.functions["function"][c1])) {
processFuncObj(config.functions.function[c1]); processFuncObj(config.functions["function"][c1]);
} }
c1 += 1; c1 += 1;
......
...@@ -6,13 +6,13 @@ define('GLabelElOutput', ['logme'], function (logme) { ...@@ -6,13 +6,13 @@ define('GLabelElOutput', ['logme'], function (logme) {
return GLabelElOutput; return GLabelElOutput;
function GLabelElOutput(config, state) { function GLabelElOutput(config, state) {
if ($.isPlainObject(config.functions.function)) { if ($.isPlainObject(config.functions["function"])) {
processFuncObj(config.functions.function); processFuncObj(config.functions["function"]);
} else if ($.isArray(config.functions.function)) { } else if ($.isArray(config.functions["function"])) {
(function (c1) { (function (c1) {
while (c1 < config.functions.function.length) { while (c1 < config.functions["function"].length) {
if ($.isPlainObject(config.functions.function[c1])) { if ($.isPlainObject(config.functions["function"][c1])) {
processFuncObj(config.functions.function[c1]); processFuncObj(config.functions["function"][c1]);
} }
c1 += 1; c1 += 1;
......
...@@ -838,33 +838,33 @@ define('Graph', ['logme'], function (logme) { ...@@ -838,33 +838,33 @@ define('Graph', ['logme'], function (logme) {
return; return;
} }
if (typeof config.functions.function === 'string') { if (typeof config.functions["function"] === 'string') {
// If just one function string is present. // If just one function string is present.
addFunction(config.functions.function); addFunction(config.functions["function"]);
} else if ($.isPlainObject(config.functions.function) === true) { } else if ($.isPlainObject(config.functions["function"]) === true) {
// If a function is present, but it also has properties // If a function is present, but it also has properties
// defined. // defined.
callAddFunction(config.functions.function); callAddFunction(config.functions["function"]);
} else if ($.isArray(config.functions.function)) { } else if ($.isArray(config.functions["function"])) {
// If more than one function is defined. // If more than one function is defined.
for (c1 = 0; c1 < config.functions.function.length; c1 += 1) { for (c1 = 0; c1 < config.functions["function"].length; c1 += 1) {
// For each definition, we must check if it is a simple // For each definition, we must check if it is a simple
// string definition, or a complex one with properties. // string definition, or a complex one with properties.
if (typeof config.functions.function[c1] === 'string') { if (typeof config.functions["function"][c1] === 'string') {
// Simple string. // Simple string.
addFunction(config.functions.function[c1]); addFunction(config.functions["function"][c1]);
} else if ($.isPlainObject(config.functions.function[c1])) { } else if ($.isPlainObject(config.functions["function"][c1])) {
// Properties are present. // Properties are present.
callAddFunction(config.functions.function[c1]); callAddFunction(config.functions["function"][c1]);
} }
} }
......
...@@ -20,13 +20,17 @@ class @HTMLEditingDescriptor ...@@ -20,13 +20,17 @@ class @HTMLEditingDescriptor
theme : "advanced", theme : "advanced",
skin: 'studio', skin: 'studio',
schema: "html5", schema: "html5",
# Necessary to preserve relative URLs to our images.
convert_urls : false,
# TODO: we should share this CSS with studio (and LMS) # TODO: we should share this CSS with studio (and LMS)
content_css : "/static/css/tiny-mce.css", content_css : "/static/css/tiny-mce.css",
# Disable h4, h5, and h6 styles as we don't have CSS for them.
formats : { formats : {
# Disable h4, h5, and h6 styles as we don't have CSS for them.
h4: {}, h4: {},
h5: {}, h5: {},
h6: {} h6: {},
# tinyMCE does block level for code by default
code: {inline: 'code'}
}, },
# Disable visual aid on borderless table. # Disable visual aid on borderless table.
visual:false, visual:false,
...@@ -50,7 +54,7 @@ class @HTMLEditingDescriptor ...@@ -50,7 +54,7 @@ class @HTMLEditingDescriptor
@setupTinyMCE: (ed) -> @setupTinyMCE: (ed) ->
ed.addButton('wrapAsCode', { ed.addButton('wrapAsCode', {
title : 'Code Block', title : 'Code',
image : '/static/images/ico-tinymce-code.png', image : '/static/images/ico-tinymce-code.png',
onclick : () -> onclick : () ->
ed.formatter.toggle('code') ed.formatter.toggle('code')
......
...@@ -45,6 +45,7 @@ class @VideoPlayer extends Subview ...@@ -45,6 +45,7 @@ class @VideoPlayer extends Subview
modestbranding: 1 modestbranding: 1
if @video.start if @video.start
@playerVars.start = @video.start @playerVars.start = @video.start
@playerVars.wmode = 'window'
if @video.end if @video.end
# work in AS3, not HMLT5. but iframe use AS3 # work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end @playerVars.end = @video.end
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Circuit Schematic display_name: Circuit Schematic
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
data: | data: |
<problem > <problem >
Please make a voltage divider that splits the provided voltage evenly. Please make a voltage divider that splits the provided voltage evenly.
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Custom Grader display_name: Custom Grader
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -4,6 +4,8 @@ metadata: ...@@ -4,6 +4,8 @@ metadata:
rerandomize: never rerandomize: never
showanswer: always showanswer: always
markdown: "" markdown: ""
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
</problem> </problem>
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Formula Response display_name: Formula Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Image Response display_name: Image Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Multiple Choice display_name: Multiple Choice
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
markdown: markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single "A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early option presented. Multiple Choice questions have been the subject of many areas of research due to the early
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Numerical Response display_name: Numerical Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
markdown: markdown:
"A numerical response problem accepts a line of text input from the "A numerical response problem accepts a line of text input from the
student, and evaluates the input for correctness based on its student, and evaluates the input for correctness based on its
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: Option Response display_name: Option Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
markdown: markdown:
"OptionResponse gives a limited set of options for students to respond with, and presents those options "OptionResponse gives a limited set of options for students to respond with, and presents those options
in a format that encourages them to search for a specific answer rather than being immediately presented in a format that encourages them to search for a specific answer rather than being immediately presented
......
...@@ -3,6 +3,8 @@ metadata: ...@@ -3,6 +3,8 @@ metadata:
display_name: String Response display_name: String Response
rerandomize: never rerandomize: never
showanswer: always showanswer: always
weight: ""
attempts: ""
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding # Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown: markdown:
"A string response problem accepts a line of text input from the "A string response problem accepts a line of text input from the
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<script type="text/javascript" src="${static.url('js/vendor/RequireJS.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/RequireJS.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-ui.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-ui.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.ui.draggable.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/swfobject/swfobject.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/swfobject/swfobject.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.cookie.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery.cookie.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
......
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline -e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git://github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
...@@ -236,7 +236,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -236,7 +236,7 @@ def index(request, course_id, chapter=None, section=None,
# Load all descendents of the section, because we're going to display it's # Load all descendents of the section, because we're going to display it's
# html, which in general will need all of its children # html, which in general will need all of its children
section_module = get_module(request.user, request, section_descriptor.location, section_module = get_module(request.user, request, section_descriptor.location,
student_module_cache, course.id, depth=None) student_module_cache, course.id, position=position, depth=None)
if section_module is None: if section_module is None:
# User may be trying to be clever and access something # User may be trying to be clever and access something
# they don't have access to. # they don't have access to.
......
...@@ -62,14 +62,3 @@ class Permission(models.Model): ...@@ -62,14 +62,3 @@ class Permission(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@receiver(post_save, sender=CourseEnrollment)
def assign_default_role(sender, instance, **kwargs):
if instance.user.is_staff:
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
else:
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role)
...@@ -88,3 +88,9 @@ PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_ ...@@ -88,3 +88,9 @@ PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_
PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
# Pearson hash for import/export
PEARSON = AUTH_TOKENS.get("PEARSON")
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
...@@ -24,8 +24,8 @@ ...@@ -24,8 +24,8 @@
<section class="bottom"> <section class="bottom">
<section class="copyright"> <section class="copyright">
## <p>&copy; 2012 edX, <a href="${reverse('copyright')}">some rights reserved.</a></p> # TODO: (bridger) Update the copyright for edX, then re-enable the link. ## <p>&copy; 2013 edX, <a href="${reverse('copyright')}">some rights reserved.</a></p> # TODO: (bridger) Update the copyright for edX, then re-enable the link.
<p>&copy; 2012 edX, some rights reserved.</p> <p>&copy; 2013 edX, some rights reserved.</p>
</section> </section>
<section class="secondary"> <section class="secondary">
......
...@@ -466,7 +466,7 @@ ...@@ -466,7 +466,7 @@
<span class="label">Last Eligible Appointment Date:</span> <span class="value">${exam_info.last_eligible_appointment_date_text}</span> <span class="label">Last Eligible Appointment Date:</span> <span class="value">${exam_info.last_eligible_appointment_date_text}</span>
</li> </li>
<li> <li>
<span class="label">Registration End Date:</span> <span class="value">${exam_info.registration_end_date_text}</span> <span class="label">Registration Ends:</span> <span class="value">${exam_info.registration_end_date_text}</span>
</li> </li>
</ul> </ul>
% endif % endif
......
...@@ -58,4 +58,4 @@ factory_boy ...@@ -58,4 +58,4 @@ factory_boy
Shapely==1.2.16 Shapely==1.2.16
ipython==0.13.1 ipython==0.13.1
xmltodict==0.4.1 xmltodict==0.4.1
paramiko==1.9.0
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