Commit dbf58c0d by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/export-draft-modules

parents 5f9d7db9 aca6cdba
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor
from request_cache.middleware import RequestCache
class Command(BaseCommand):
help = '''Enumerates through the course and find common errors'''
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("check_course requires one argument: <location>")
loc_str = args[0]
loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore()
# setup a request cache so we don't throttle the DB with all the metadata inheritance requests
store.request_cache = RequestCache.get_request_cache()
course = store.get_item(loc, depth=3)
err_cnt = 0
def _xlint_metadata(module):
err_cnt = check_module_metadata_editability(module)
for child in module.get_children():
err_cnt = err_cnt + _xlint_metadata(child)
return err_cnt
err_cnt = err_cnt + _xlint_metadata(course)
# we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict
def _check_xml_attributes_field(module):
err_cnt = 0
if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring):
print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url())
err_cnt = err_cnt + 1
for child in module.get_children():
err_cnt = err_cnt + _check_xml_attributes_field(child)
return err_cnt
err_cnt = err_cnt + _check_xml_attributes_field(course)
# check for dangling discussion items, this can cause errors in the forums
def _get_discussion_items(module):
discussion_items = []
if module.location.category == 'discussion':
discussion_items = discussion_items + [module.location.url()]
for child in module.get_children():
discussion_items = discussion_items + _get_discussion_items(child)
return discussion_items
discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
'discussion', None, None])
for item in queried_discussion_items:
if item.location.url() not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url())
...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') ...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value MITX_FEATURES[feature] = value
# load segment.io key, provide a dummy if it does not exist
SEGMENT_IO_KEY = ENV_TOKENS.get('SEGMENT_IO_KEY', '***REMOVED***')
LOGGING = get_logger_config(LOG_DIR, LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'], logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
......
...@@ -34,7 +34,9 @@ MITX_FEATURES = { ...@@ -34,7 +34,9 @@ MITX_FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True, 'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -150,3 +150,6 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True ...@@ -150,3 +150,6 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode # disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg'
...@@ -118,3 +118,6 @@ PASSWORD_HASHERS = ( ...@@ -118,3 +118,6 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
) )
# dummy segment-io key
SEGMENT_IO_KEY = '***REMOVED***'
...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
clickSaveButton: (event) => clickSaveButton: (event) =>
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
analytics.track "Saved Module",
course: course_location_analytics
id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @metadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
......
...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
tabs.push($(element).data('id')) tabs.push($(element).data('id'))
) )
analytics.track "Reordered Static Pages",
course: course_location_analytics
$.ajax({ $.ajax({
type:'POST', type:'POST',
url: '/reorder_static_tabs', url: '/reorder_static_tabs',
...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View
'i4x://edx/templates/static_tab/Empty' 'i4x://edx/templates/static_tab/Empty'
) )
analytics.track "Added Static Page",
course: course_location_analytics
deleteTab: (event) => deleteTab: (event) =>
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
return return
$component = $(event.currentTarget).parents('.component') $component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
......
...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' handle: '.drag-handle'
update: (event, ui) => update: (event, ui) =>
analytics.track "Reordered Components",
course: course_location_analytics
id: unit_location_analytics
payload = children : @components() payload = children : @components()
options = success : => @model.unset('children') options = success : => @model.unset('children')
@model.save(payload, options) @model.save(payload, options)
...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$(event.currentTarget).data('location') $(event.currentTarget).data('location')
) )
analytics.track "Added a Component",
course: course_location_analytics
unit_id: unit_location_analytics
type: $(event.currentTarget).data('location')
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
analytics.track "Deleted a Component",
course: course_location_analytics
unit_id: unit_location_analytics
id: $component.data('id')
$component.remove() $component.remove()
# b/c we don't vigilantly keep children up to date # b/c we don't vigilantly keep children up to date
# get rid of it before it hurts someone # get rid of it before it hurts someone
...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View
id: @$el.data('id') id: @$el.data('id')
delete_children: true delete_children: true
}, => }, =>
analytics.track "Deleted Draft",
course: course_location_analytics
unit_id: unit_location_analytics
window.location.reload() window.location.reload()
) )
...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/create_draft', { $.post('/create_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Created Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'draft') @model.set('state', 'draft')
) )
...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/publish_draft', { $.post('/publish_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Published Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'public') @model.set('state', 'public')
) )
setVisibility: (event) -> setVisibility: (event) ->
if @$('.visibility-select').val() == 'private' if @$('.visibility-select').val() == 'private'
target_url = '/unpublish_unit' target_url = '/unpublish_unit'
visibility = "private"
else else
target_url = '/publish_draft' target_url = '/publish_draft'
visibility = "public"
@wait(true) @wait(true)
$.post(target_url, { $.post(target_url, {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Set Unit Visibility",
course: course_location_analytics
unit_id: unit_location_analytics
visibility: visibility
@model.set('state', @$('.visibility-select').val()) @model.set('state', @$('.visibility-select').val())
) )
...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View ...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
@model.save(metadata: metadata) @model.save(metadata: metadata)
# Update name shown in the right-hand side location summary. # Update name shown in the right-hand side location summary.
$('.unit-location .editing .unit-name').html(metadata.display_name) $('.unit-location .editing .unit-name').html(metadata.display_name)
analytics.track "Edited Unit Name",
course: course_location_analytics
unit_id: unit_location_analytics
display_name: metadata.display_name
class CMS.Views.UnitEdit.LocationState extends Backbone.View class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: => initialize: =>
......
...@@ -331,6 +331,12 @@ function createNewUnit(e) { ...@@ -331,6 +331,12 @@ function createNewUnit(e) {
var parent = $(this).data('parent'); var parent = $(this).data('parent');
var template = $(this).data('template'); var template = $(this).data('template');
analytics.track('Created a Unit', {
'course': course_location_analytics,
'parent_location': parent
});
$.post('/clone_item', $.post('/clone_item',
{'parent_location': parent, {'parent_location': parent,
'template': template, 'template': template,
...@@ -363,6 +369,12 @@ function _deleteItem($el) { ...@@ -363,6 +369,12 @@ function _deleteItem($el) {
var id = $el.data('id'); var id = $el.data('id');
analytics.track('Deleted an Item', {
'course': course_location_analytics,
'id': id
});
$.post('/delete_item', $.post('/delete_item',
{'id': id, 'delete_children': true, 'delete_all_versions': true}, {'id': id, 'delete_children': true, 'delete_all_versions': true},
function (data) { function (data) {
...@@ -426,6 +438,11 @@ function displayFinishedUpload(xhr) { ...@@ -426,6 +438,11 @@ function displayFinishedUpload(xhr) {
var html = Mustache.to_html(template, resp); var html = Mustache.to_html(template, resp);
$('table > tbody').prepend(html); $('table > tbody').prepend(html);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': resp.url
});
} }
function markAsLoaded() { function markAsLoaded() {
...@@ -555,6 +572,11 @@ function saveNewSection(e) { ...@@ -555,6 +572,11 @@ function saveNewSection(e) {
var template = $saveButton.data('template'); var template = $saveButton.data('template');
var display_name = $(this).find('.new-section-name').val(); var display_name = $(this).find('.new-section-name').val();
analytics.track('Created a Section', {
'course': course_location_analytics,
'display_name': display_name
});
$.post('/clone_item', { $.post('/clone_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'template': template,
...@@ -600,6 +622,12 @@ function saveNewCourse(e) { ...@@ -600,6 +622,12 @@ function saveNewCourse(e) {
return; return;
} }
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name
});
$.post('/create_new_course', { $.post('/create_new_course', {
'template': template, 'template': template,
'org': org, 'org': org,
...@@ -646,9 +674,14 @@ function saveNewSubsection(e) { ...@@ -646,9 +674,14 @@ function saveNewSubsection(e) {
var parent = $(this).find('.new-subsection-name-save').data('parent'); var parent = $(this).find('.new-subsection-name-save').data('parent');
var template = $(this).find('.new-subsection-name-save').data('template'); var template = $(this).find('.new-subsection-name-save').data('template');
var display_name = $(this).find('.new-subsection-name-input').val(); var display_name = $(this).find('.new-subsection-name-input').val();
analytics.track('Created a Subsection', {
'course': course_location_analytics,
'display_name': display_name
});
$.post('/clone_item', { $.post('/clone_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'template': template,
...@@ -702,6 +735,13 @@ function saveEditSectionName(e) { ...@@ -702,6 +735,13 @@ function saveEditSectionName(e) {
return; return;
} }
analytics.track('Edited Section Name', {
'course': course_location_analytics,
'display_name': display_name,
'id': id
});
var $_this = $(this); var $_this = $(this);
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
...@@ -741,6 +781,12 @@ function saveSetSectionScheduleDate(e) { ...@@ -741,6 +781,12 @@ function saveSetSectionScheduleDate(e) {
var id = $modal.attr('data-id'); var id = $modal.attr('data-id');
analytics.track('Edited Section Release Date', {
'course': course_location_analytics,
'id': id,
'start': start
});
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
url: "/save_item", url: "/save_item",
......
...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({
var task_index = $checkbox.data('task'); var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index); var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed); model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({}, model.save({},
{ {
success : function() { success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index); var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate); self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
analytics.track('Toggled a Checklist Task', {
'course': course_location_analytics,
'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
......
...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// push change to display, hide the editor, submit the change // push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError}); targetModel.save({}, {error : CMS.ServerError});
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
});
}, },
onCancel: function(event) { onCancel: function(event) {
...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
return; return;
} }
analytics.track('Deleted Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
});
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
this.model.save({}, {error: CMS.ServerError}); this.model.save({}, {error: CMS.ServerError});
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
});
}, },
onCancel: function(event) { onCancel: function(event) {
......
...@@ -137,6 +137,10 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -137,6 +137,10 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
success : function() { success : function() {
self.render(); self.render();
self.showMessage(self.successful_changes); self.showMessage(self.successful_changes);
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
@import 'elements/forms'; @import 'elements/forms';
@import 'elements/modal'; @import 'elements/modal';
@import 'elements/alerts'; @import 'elements/alerts';
@import 'elements/jquery-ui-calendar'; @import 'elements/vendor';
@import 'elements/tender-widget'; @import 'elements/tender-widget';
// specific views // specific views
......
// studio - elements - JQUI calendar // studio - elements - vendor overrides
// ==================== // ====================
// JQUI calendar
.ui-datepicker { .ui-datepicker {
border-color: $darkGrey; border-color: $darkGrey;
border-radius: 2px; border-radius: 2px;
...@@ -55,3 +56,10 @@ ...@@ -55,3 +56,10 @@
color: #fff; color: #fff;
} }
} }
// ====================
// JQUI timepicker
.ui-timepicker-list {
z-index: 100000 !important;
}
\ No newline at end of file
...@@ -26,7 +26,7 @@ body.course.outline { ...@@ -26,7 +26,7 @@ body.course.outline {
position: relative; position: relative;
top: -4px; top: -4px;
right: 50px; right: 50px;
width: 145px; width: 100px;
.status-label { .status-label {
position: absolute; position: absolute;
...@@ -62,7 +62,7 @@ body.course.outline { ...@@ -62,7 +62,7 @@ body.course.outline {
opacity: 0.0; opacity: 0.0;
position: absolute; position: absolute;
top: -1px; top: -1px;
left: 5px; right: 0;
margin: 0; margin: 0;
padding: 8px 12px; padding: 8px 12px;
background: $white; background: $white;
...@@ -160,7 +160,7 @@ body.course.outline { ...@@ -160,7 +160,7 @@ body.course.outline {
.section-published-date { .section-published-date {
position: absolute; position: absolute;
top: 19px; top: 19px;
right: 90px; right: 80px;
padding: 4px 10px; padding: 4px 10px;
border-radius: 3px; border-radius: 3px;
background: $lightGrey; background: $lightGrey;
...@@ -271,8 +271,6 @@ body.course.outline { ...@@ -271,8 +271,6 @@ body.course.outline {
.section-published-date { .section-published-date {
float: right; float: right;
width: 278px;
margin-right: 220px;
@include border-radius(3px); @include border-radius(3px);
background: $lightGrey; background: $lightGrey;
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%include file="widgets/segment-io.html" />
<%block name="header_extras"></%block> <%block name="header_extras"></%block>
</head> </head>
......
...@@ -46,6 +46,8 @@ ...@@ -46,6 +46,8 @@
<li class="nav-item"> <li class="nav-item">
% if not disable_course_creation: % if not disable_course_creation:
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a> <a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a>
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">Email staff to create course</a>
% endif % endif
</li> </li>
</ul> </ul>
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
<article class="my-classes"> <article class="my-classes">
% if user.is_active: % if user.is_active:
<ul class="class-list"> <ul class="class-list">
%for course, url, lms_link in courses: %for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower()):
<li> <li>
<a class="class-link" href="${url}" class="class-name"> <a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span> <span class="class-name">${course}</span>
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
}); });
}); });
var unit_location_analytics = '${unit_location}';
</script> </script>
</%block> </%block>
......
% if settings.MITX_FEATURES.get('SEGMENT_IO'):
<!-- begin Segment.io -->
<script type="text/javascript">
// if inside course, inject the course location into the JS namespace
%if context_course:
var course_location_analytics = "${context_course.location}";
%endif
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
analytics.load("${ settings.SEGMENT_IO_KEY }");
% if user.is_authenticated():
analytics.identify("${ user.id }", {
email : "${ user.email }",
username : "${ user.username }"
});
% endif
</script>
<!-- end Segment.io -->
% else:
<!-- dummy segment.io -->
<script type="text/javascript">
%if context_course:
var course_location_analytics = "${context_course.location}";
%endif
var analytics = {
track: function() { return; }
};
</script>
<!-- end dummy segment.io -->
% endif
...@@ -655,9 +655,9 @@ class MatlabInput(CodeInput): ...@@ -655,9 +655,9 @@ class MatlabInput(CodeInput):
# Check if problem has been queued # Check if problem has been queued
self.queuename = 'matlab' self.queuename = 'matlab'
self.queue_msg = '' self.queue_msg = ''
if 'queue_msg' in self.input_state and self.status in ['queued','incomplete', 'unsubmitted']: if 'queue_msg' in self.input_state and self.status in ['queued', 'incomplete', 'unsubmitted']:
self.queue_msg = self.input_state['queue_msg'] self.queue_msg = self.input_state['queue_msg']
if 'queued' in self.input_state and self.input_state['queuestate'] is not None: if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
self.status = 'queued' self.status = 'queued'
self.queue_len = 1 self.queue_len = 1
self.msg = self.plot_submitted_msg self.msg = self.plot_submitted_msg
...@@ -702,7 +702,7 @@ class MatlabInput(CodeInput): ...@@ -702,7 +702,7 @@ class MatlabInput(CodeInput):
def _extra_context(self): def _extra_context(self):
''' Set up additional context variables''' ''' Set up additional context variables'''
extra_context = { extra_context = {
'queue_len': self.queue_len, 'queue_len': str(self.queue_len),
'queue_msg': self.queue_msg 'queue_msg': self.queue_msg
} }
return extra_context return extra_context
......
...@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase): ...@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, } 'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml) elt = etree.fromstring(self.xml)
input_class = lookup_tag('matlabinput')
the_input = self.input_class(test_system, elt, state) the_input = self.input_class(test_system, elt, state)
context = the_input._get_render_context() context = the_input._get_render_context()
...@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase): ...@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase):
self.assertEqual(context, expected) self.assertEqual(context, expected)
def test_rendering_while_queued(self):
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': {'queuestate': 'queued'},
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': self.input_class.plot_submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '1',
}
self.assertEqual(context, expected)
def test_plot_data(self): def test_plot_data(self):
get = {'submission': 'x = 1234;'} get = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", get) response = self.the_input.handle_ajax("plot", get)
...@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase): ...@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase):
self.assertTrue(self.the_input.input_state['queuekey'] is not None) self.assertTrue(self.the_input.input_state['queuekey'] is not None)
self.assertEqual(self.the_input.input_state['queuestate'], 'queued') self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
def test_ungraded_response_success(self):
queuekey = 'abcd'
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': input_state,
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
the_input.ungraded_response(queue_msg, queuekey)
self.assertTrue(input_state['queuekey'] is None)
self.assertTrue(input_state['queuestate'] is None)
self.assertEqual(input_state['queue_msg'], inner_msg)
def test_ungraded_response_key_mismatch(self):
queuekey = 'abcd'
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': input_state,
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
the_input.ungraded_response(queue_msg, 'abc')
self.assertEqual(input_state['queuekey'], queuekey)
self.assertEqual(input_state['queuestate'], 'queued')
self.assertFalse('queue_msg' in input_state)
......
...@@ -33,7 +33,7 @@ def group_from_value(groups, v): ...@@ -33,7 +33,7 @@ def group_from_value(groups, v):
class ABTestFields(object): class ABTestFields(object):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content) group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={}) group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []}) group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content) experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True has_children = True
......
...@@ -83,7 +83,7 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -83,7 +83,7 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object): class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state) attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings) max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
...@@ -91,12 +91,12 @@ class CapaFields(object): ...@@ -91,12 +91,12 @@ class CapaFields(object):
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False) force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={}) correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state) input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state) student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
seed = StringyInteger(help="Random seed for this student", scope=Scope.student_state) seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
markdown = String(help="Markdown source of this module", scope=Scope.settings) markdown = String(help="Markdown source of this module", scope=Scope.settings)
...@@ -108,7 +108,6 @@ class CapaModule(CapaFields, XModule): ...@@ -108,7 +108,6 @@ class CapaModule(CapaFields, XModule):
''' '''
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'),
...@@ -388,7 +387,6 @@ class CapaModule(CapaFields, XModule): ...@@ -388,7 +387,6 @@ class CapaModule(CapaFields, XModule):
return html return html
def get_problem_html(self, encapsulate=True): def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons '''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.''' as necessary based on the problem config and state.'''
...@@ -401,7 +399,6 @@ class CapaModule(CapaFields, XModule): ...@@ -401,7 +399,6 @@ class CapaModule(CapaFields, XModule):
except Exception, err: except Exception, err:
html = self.handle_problem_html_error(err) html = self.handle_problem_html_error(err)
# The convention is to pass the name of the check button # The convention is to pass the name of the check button
# if we want to show a check button, and False otherwise # if we want to show a check button, and False otherwise
# This works because non-empty strings evaluate to True # This works because non-empty strings evaluate to True
...@@ -535,7 +532,6 @@ class CapaModule(CapaFields, XModule): ...@@ -535,7 +532,6 @@ class CapaModule(CapaFields, XModule):
return False return False
def update_score(self, get): def update_score(self, get):
""" """
Delivers grading response (e.g. from asynchronous code checking) to Delivers grading response (e.g. from asynchronous code checking) to
...@@ -590,7 +586,6 @@ class CapaModule(CapaFields, XModule): ...@@ -590,7 +586,6 @@ class CapaModule(CapaFields, XModule):
self.set_state_from_lcp() self.set_state_from_lcp()
return response return response
def get_answer(self, get): def get_answer(self, get):
''' '''
For the "show answer" button. For the "show answer" button.
...@@ -700,7 +695,6 @@ class CapaModule(CapaFields, XModule): ...@@ -700,7 +695,6 @@ class CapaModule(CapaFields, XModule):
'max_value': score['total'], 'max_value': score['total'],
}) })
def check_problem(self, get): def check_problem(self, get):
''' Checks whether answers to a problem are correct, and ''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers: returns a map of correct/incorrect answers:
...@@ -783,7 +777,7 @@ class CapaModule(CapaFields, XModule): ...@@ -783,7 +777,7 @@ class CapaModule(CapaFields, XModule):
self.system.track_function('save_problem_check', event_info) self.system.track_function('save_problem_check', event_info)
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state()) self.system.psychometrics_handler(self.get_state_for_lcp())
# render problem into HTML # render problem into HTML
html = self.get_problem_html(encapsulate=False) html = self.get_problem_html(encapsulate=False)
......
...@@ -50,14 +50,14 @@ class VersionInteger(Integer): ...@@ -50,14 +50,14 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state) current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.student_state) scope=Scope.user_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.student_state) scope=Scope.user_state)
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.student_state) scope=Scope.user_state)
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
...@@ -219,4 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -219,4 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
...@@ -125,7 +125,8 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -125,7 +125,8 @@ class ConditionalModule(ConditionalFields, XModule):
an AJAX call. an AJAX call.
""" """
if not self.is_condition_satisfied(): if not self.is_condition_satisfied():
message = self.descriptor.xml_attributes.get('message') defmsg = "{link} must be attempted before this will become visible."
message = self.descriptor.xml_attributes.get('message', defmsg)
context = {'module': self, context = {'module': self,
'message': message} 'message': message}
html = self.system.render_template('conditional_module.html', html = self.system.render_template('conditional_module.html',
......
...@@ -652,7 +652,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -652,7 +652,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def end_date_text(self): def end_date_text(self):
return time.strftime("%b %d, %Y", self.end) """
Returns the end date for the course formatted as a string.
If the course does not have an end date set (course.end is None), an empty string will be returned.
"""
return '' if self.end is None else time.strftime("%b %d, %Y", self.end)
@property @property
def forum_posts_allowed(self): def forum_posts_allowed(self):
......
...@@ -422,22 +422,44 @@ def remap_namespace(module, target_location_namespace): ...@@ -422,22 +422,44 @@ def remap_namespace(module, target_location_namespace):
return module return module
def validate_no_non_editable_metadata(module_store, course_id, category, allowed=None):
def allowed_metadata_by_category(category):
# should this be in the descriptors?!?
return {
'vertical': [],
'chapter': ['start'],
'sequential': ['due', 'format', 'start', 'graded']
}.get(category,['*'])
def check_module_metadata_editability(module):
''' '''
Assert that there is no metadata within a particular category that we can't support editing Assert that there is no metadata within a particular module that we can't support editing
However we always allow 'display_name' and 'xml_attribtues' However we always allow 'display_name' and 'xml_attribtues'
''' '''
_allowed = (allowed if allowed is not None else []) + ['xml_attributes', 'display_name'] allowed = allowed_metadata_by_category(module.location.category)
if '*' in allowed:
# everything is allowed
return 0
allowed = allowed + ['xml_attributes', 'display_name']
err_cnt = 0
my_metadata = dict(own_metadata(module))
illegal_keys = set(own_metadata(module).keys()) - set(allowed)
if len(illegal_keys) > 0:
err_cnt = err_cnt + 1
print ': found non-editable metadata on {0}. These metadata keys are not supported = {1}'. format(module.location.url(), illegal_keys)
return err_cnt
def validate_no_non_editable_metadata(module_store, course_id, category):
err_cnt = 0 err_cnt = 0
for module_loc in module_store.modules[course_id]: for module_loc in module_store.modules[course_id]:
module = module_store.modules[course_id][module_loc] module = module_store.modules[course_id][module_loc]
if module.location.category == category: if module.location.category == category:
my_metadata = dict(own_metadata(module)) err_cnt = err_cnt + check_module_metadata_editability(module)
for key in my_metadata.keys():
if key not in _allowed:
err_cnt = err_cnt + 1
print ': found metadata on {0}. Studio will not support editing this piece of metadata, so it is not allowed. Metadata: {1} = {2}'. format(module.location.url(), key, my_metadata[key])
return err_cnt return err_cnt
...@@ -529,10 +551,9 @@ def perform_xlint(data_dir, course_dirs, ...@@ -529,10 +551,9 @@ def perform_xlint(data_dir, course_dirs,
# don't allow metadata on verticals, since we can't edit them in studio # don't allow metadata on verticals, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical") err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical")
# don't allow metadata on chapters, since we can't edit them in studio # don't allow metadata on chapters, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter",['start']) err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter")
# don't allow metadata on sequences that we can't edit # don't allow metadata on sequences that we can't edit
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential", err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential")
['due','format','start','graded'])
# check for a presence of a course marketing video # check for a presence of a course marketing video
location_elements = course_id.split('/') location_elements = course_id.split('/')
......
...@@ -37,8 +37,8 @@ class PeerGradingFields(object): ...@@ -37,8 +37,8 @@ class PeerGradingFields(object):
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
scope=Scope.settings) scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}), student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.student_state) scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
...@@ -577,4 +577,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -577,4 +577,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True
template_dir_name = "peer_grading" template_dir_name = "peer_grading"
...@@ -30,8 +30,8 @@ class PollFields(object): ...@@ -30,8 +30,8 @@ class PollFields(object):
# Name of poll to use in links to this poll # Name of poll to use in links to this poll
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False) voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
poll_answer = String(help="Student answer", scope=Scope.student_state, default='') poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content) poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
answers = List(help="Poll answers from xml", scope=Scope.content, default=[]) answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
......
...@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__) ...@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__)
class RandomizeFields(object): class RandomizeFields(object):
choice = Integer(help="Which random child was chosen", scope=Scope.student_state) choice = Integer(help="Which random child was chosen", scope=Scope.user_state)
class RandomizeModule(RandomizeFields, XModule): class RandomizeModule(RandomizeFields, XModule):
......
...@@ -23,7 +23,7 @@ class SequenceFields(object): ...@@ -23,7 +23,7 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student # NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix. # positions saved on prod, so it's not easy to fix.
position = Integer(help="Last tab viewed in this sequence", scope=Scope.student_state) position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state)
class SequenceModule(SequenceFields, XModule): class SequenceModule(SequenceFields, XModule):
......
import unittest import unittest
from time import strptime from time import strptime
import datetime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
import xmodule.course_module
from xmodule.util.date_utils import time_to_datetime
ORG = 'test_org' ORG = 'test_org'
...@@ -39,8 +42,19 @@ class DummySystem(ImportSystem): ...@@ -39,8 +42,19 @@ class DummySystem(ImportSystem):
class IsNewCourseTestCase(unittest.TestCase): class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses""" """Make sure the property is_new works on courses"""
def setUp(self):
# Needed for test_is_newish
datetime_patcher = patch.object(
xmodule.course_module, 'datetime',
Mock(wraps=datetime.datetime)
)
mocked_datetime = datetime_patcher.start()
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
self.addCleanup(datetime_patcher.stop)
@staticmethod @staticmethod
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None): def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
"""Get a dummy course""" """Get a dummy course"""
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
...@@ -51,6 +65,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -51,6 +65,7 @@ class IsNewCourseTestCase(unittest.TestCase):
is_new = to_attrb('is_new', is_new) is_new = to_attrb('is_new', is_new)
announcement = to_attrb('announcement', announcement) announcement = to_attrb('announcement', announcement)
advertised_start = to_attrb('advertised_start', advertised_start) advertised_start = to_attrb('advertised_start', advertised_start)
end = to_attrb('end', end)
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
...@@ -58,13 +73,14 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -58,13 +73,14 @@ class IsNewCourseTestCase(unittest.TestCase):
start="{start}" start="{start}"
{announcement} {announcement}
{is_new} {is_new}
{advertised_start}> {advertised_start}
{end}>
<chapter url="hi" url_name="ch" display_name="CH"> <chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html> <html url_name="h" display_name="H">Two houses, ...</html>
</chapter> </chapter>
</course> </course>
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new, '''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
announcement=announcement, advertised_start=advertised_start) announcement=announcement, advertised_start=advertised_start, end=end)
return system.process_xml(start_xml) return system.process_xml(start_xml)
...@@ -126,10 +142,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -126,10 +142,7 @@ class IsNewCourseTestCase(unittest.TestCase):
print "Checking start=%s advertised=%s" % (s[0], s[1]) print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2]) self.assertEqual(d.start_date_text, s[2])
@patch('xmodule.course_module.time.gmtime') def test_is_newish(self):
def test_is_newish(self, gmtime_mock):
gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
...@@ -150,3 +163,11 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -150,3 +163,11 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = self.get_dummy_course(start='2012-12-31T12:00') descriptor = self.get_dummy_course(start='2012-12-31T12:00')
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
def test_end_date_text(self):
# No end date set, returns empty string.
d = self.get_dummy_course('2012-12-02T12:00')
self.assertEqual('', d.end_date_text)
d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014', d.end_date_text)
...@@ -16,9 +16,9 @@ log = logging.getLogger(__name__) ...@@ -16,9 +16,9 @@ log = logging.getLogger(__name__)
class TimeLimitFields(object): class TimeLimitFields(object):
beginning_at = Float(help="The time this timer was started", scope=Scope.student_state) beginning_at = Float(help="The time this timer was started", scope=Scope.user_state)
ending_at = Float(help="The time this timer will end", scope=Scope.student_state) ending_at = Float(help="The time this timer will end", scope=Scope.user_state)
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.student_state) accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.user_state)
time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings) time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings)
duration = Float(help="The length of this timer", scope=Scope.settings) duration = Float(help="The length of this timer", scope=Scope.settings)
suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings) suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings)
......
...@@ -19,7 +19,7 @@ log = logging.getLogger(__name__) ...@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
......
...@@ -21,7 +21,7 @@ log = logging.getLogger(__name__) ...@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
class VideoAlphaFields(object): class VideoAlphaFields(object):
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
......
Test course for checking the end date displayed on the course about page.
This course has both an end_date HTML "blob", and it also has a course end date set.
The end_date "blob" has higher precedence and will show.
See also test_end course.
<course org="edX" course="test_about_blob_end_date" url_name="2012_Fall"/>
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"end": "2015-09-17T12:00",
"display_name": "Test About Blob End Date",
"graded": "true"
}
}
Test course for checking the end date displayed on the course about page.
This course does not have an end_date HTML "blob", but it does have a course end date set.
Therefore the course end date should show on the course about page.
See also test_about_blob_end_date course.
<course org="edX" course="test_end" url_name="2012_Fall"/>
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"end": "2015-09-17T12:00",
"display_name": "Test End",
"graded": "true"
}
}
...@@ -165,7 +165,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -165,7 +165,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
# Create a fake key to pull out a StudentModule object from the ModelDataCache # Create a fake key to pull out a StudentModule object from the ModelDataCache
key = LmsKeyValueStore.Key( key = LmsKeyValueStore.Key(
Scope.student_state, Scope.user_state,
student.id, student.id,
moduledescriptor.location, moduledescriptor.location,
None None
...@@ -370,7 +370,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -370,7 +370,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
# Create a fake KeyValueStore key to pull out the StudentModule # Create a fake KeyValueStore key to pull out the StudentModule
key = LmsKeyValueStore.Key( key = LmsKeyValueStore.Key(
Scope.student_state, Scope.user_state,
user.id, user.id,
problem_descriptor.location, problem_descriptor.location,
None None
......
...@@ -76,6 +76,11 @@ class Command(BaseCommand): ...@@ -76,6 +76,11 @@ class Command(BaseCommand):
for hist_module in hist_modules: for hist_module in hist_modules:
self.remove_studentmodulehistory_input_state(hist_module, save_changes) self.remove_studentmodulehistory_input_state(hist_module, save_changes)
if self.num_visited % 1000 == 0:
LOG.info(" Progress: updated {0} of {1} student modules".format(self.num_changed, self.num_visited))
LOG.info(" Progress: updated {0} of {1} student history modules".format(self.num_hist_changed,
self.num_hist_visited))
@transaction.autocommit @transaction.autocommit
def remove_studentmodule_input_state(self, module, save_changes): def remove_studentmodule_input_state(self, module, save_changes):
''' Fix the grade assigned to a StudentModule''' ''' Fix the grade assigned to a StudentModule'''
......
...@@ -134,7 +134,7 @@ class ModelDataCache(object): ...@@ -134,7 +134,7 @@ class ModelDataCache(object):
""" """
if scope in (Scope.children, Scope.parent): if scope in (Scope.children, Scope.parent):
return [] return []
elif scope == Scope.student_state: elif scope == Scope.user_state:
return self._chunked_query( return self._chunked_query(
StudentModule, StudentModule,
'module_state_key__in', 'module_state_key__in',
...@@ -159,7 +159,7 @@ class ModelDataCache(object): ...@@ -159,7 +159,7 @@ class ModelDataCache(object):
), ),
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
elif scope == Scope.student_preferences: elif scope == Scope.preferences:
return self._chunked_query( return self._chunked_query(
XModuleStudentPrefsField, XModuleStudentPrefsField,
'module_type__in', 'module_type__in',
...@@ -167,7 +167,7 @@ class ModelDataCache(object): ...@@ -167,7 +167,7 @@ class ModelDataCache(object):
student=self.user.pk, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
elif scope == Scope.student_info: elif scope == Scope.user_info:
return self._query( return self._query(
XModuleStudentInfoField, XModuleStudentInfoField,
student=self.user.pk, student=self.user.pk,
...@@ -190,15 +190,15 @@ class ModelDataCache(object): ...@@ -190,15 +190,15 @@ class ModelDataCache(object):
""" """
Return the key used in the ModelDataCache for the specified KeyValueStore key Return the key used in the ModelDataCache for the specified KeyValueStore key
""" """
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
return (key.scope, key.block_scope_id.url()) return (key.scope, key.block_scope_id.url())
elif key.scope == Scope.content: elif key.scope == Scope.content:
return (key.scope, key.block_scope_id.url(), key.field_name) return (key.scope, key.block_scope_id.url(), key.field_name)
elif key.scope == Scope.settings: elif key.scope == Scope.settings:
return (key.scope, '%s-%s' % (self.course_id, key.block_scope_id.url()), key.field_name) return (key.scope, '%s-%s' % (self.course_id, key.block_scope_id.url()), key.field_name)
elif key.scope == Scope.student_preferences: elif key.scope == Scope.preferences:
return (key.scope, key.block_scope_id, key.field_name) return (key.scope, key.block_scope_id, key.field_name)
elif key.scope == Scope.student_info: elif key.scope == Scope.user_info:
return (key.scope, key.field_name) return (key.scope, key.field_name)
def _cache_key_from_field_object(self, scope, field_object): def _cache_key_from_field_object(self, scope, field_object):
...@@ -206,15 +206,15 @@ class ModelDataCache(object): ...@@ -206,15 +206,15 @@ class ModelDataCache(object):
Return the key used in the ModelDataCache for the specified scope and Return the key used in the ModelDataCache for the specified scope and
field field
""" """
if scope == Scope.student_state: if scope == Scope.user_state:
return (scope, field_object.module_state_key) return (scope, field_object.module_state_key)
elif scope == Scope.content: elif scope == Scope.content:
return (scope, field_object.definition_id, field_object.field_name) return (scope, field_object.definition_id, field_object.field_name)
elif scope == Scope.settings: elif scope == Scope.settings:
return (scope, field_object.usage_id, field_object.field_name) return (scope, field_object.usage_id, field_object.field_name)
elif scope == Scope.student_preferences: elif scope == Scope.preferences:
return (scope, field_object.module_type, field_object.field_name) return (scope, field_object.module_type, field_object.field_name)
elif scope == Scope.student_info: elif scope == Scope.user_info:
return (scope, field_object.field_name) return (scope, field_object.field_name)
def find(self, key): def find(self, key):
...@@ -237,13 +237,14 @@ class ModelDataCache(object): ...@@ -237,13 +237,14 @@ class ModelDataCache(object):
if field_object is not None: if field_object is not None:
return field_object return field_object
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
field_object, _ = StudentModule.objects.get_or_create( field_object, _ = StudentModule.objects.get_or_create(
course_id=self.course_id, course_id=self.course_id,
student=self.user, student=self.user,
module_type=key.block_scope_id.category,
module_state_key=key.block_scope_id.url(), module_state_key=key.block_scope_id.url(),
defaults={'state': json.dumps({})}, defaults={'state': json.dumps({}),
'module_type': key.block_scope_id.category,
},
) )
elif key.scope == Scope.content: elif key.scope == Scope.content:
field_object, _ = XModuleContentField.objects.get_or_create( field_object, _ = XModuleContentField.objects.get_or_create(
...@@ -255,13 +256,13 @@ class ModelDataCache(object): ...@@ -255,13 +256,13 @@ class ModelDataCache(object):
field_name=key.field_name, field_name=key.field_name,
usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()), usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()),
) )
elif key.scope == Scope.student_preferences: elif key.scope == Scope.preferences:
field_object, _ = XModuleStudentPrefsField.objects.get_or_create( field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
module_type=key.block_scope_id, module_type=key.block_scope_id,
student=self.user, student=self.user,
) )
elif key.scope == Scope.student_info: elif key.scope == Scope.user_info:
field_object, _ = XModuleStudentInfoField.objects.get_or_create( field_object, _ = XModuleStudentInfoField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
student=self.user, student=self.user,
...@@ -281,12 +282,12 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -281,12 +282,12 @@ class LmsKeyValueStore(KeyValueStore):
If the scope to write to is not one of the 5 named scopes: If the scope to write to is not one of the 5 named scopes:
Scope.content Scope.content
Scope.settings Scope.settings
Scope.student_state Scope.user_state
Scope.student_preferences Scope.preferences
Scope.student_info Scope.user_info
then an InvalidScopeError will be raised. then an InvalidScopeError will be raised.
Data for Scope.student_state is stored as StudentModule objects via the django orm. Data for Scope.user_state is stored as StudentModule objects via the django orm.
Data for the other scopes is stored in individual objects that are named for the Data for the other scopes is stored in individual objects that are named for the
scope involved and have the field name as a key scope involved and have the field name as a key
...@@ -297,9 +298,9 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -297,9 +298,9 @@ class LmsKeyValueStore(KeyValueStore):
_allowed_scopes = ( _allowed_scopes = (
Scope.content, Scope.content,
Scope.settings, Scope.settings,
Scope.student_state, Scope.user_state,
Scope.student_preferences, Scope.preferences,
Scope.student_info, Scope.user_info,
Scope.children, Scope.children,
) )
...@@ -321,7 +322,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -321,7 +322,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None: if field_object is None:
raise KeyError(key.field_name) raise KeyError(key.field_name)
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
return json.loads(field_object.state)[key.field_name] return json.loads(field_object.state)[key.field_name]
else: else:
return json.loads(field_object.value) return json.loads(field_object.value)
...@@ -335,7 +336,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -335,7 +336,7 @@ class LmsKeyValueStore(KeyValueStore):
if key.scope not in self._allowed_scopes: if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope) raise InvalidScopeError(key.scope)
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
state = json.loads(field_object.state) state = json.loads(field_object.state)
state[key.field_name] = value state[key.field_name] = value
field_object.state = json.dumps(state) field_object.state = json.dumps(state)
...@@ -355,7 +356,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -355,7 +356,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None: if field_object is None:
raise KeyError(key.field_name) raise KeyError(key.field_name)
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
state = json.loads(field_object.state) state = json.loads(field_object.state)
del state[key.field_name] del state[key.field_name]
field_object.state = json.dumps(state) field_object.state = json.dumps(state)
...@@ -377,7 +378,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -377,7 +378,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None: if field_object is None:
return False return False
if key.scope == Scope.student_state: if key.scope == Scope.user_state:
return key.field_name in json.loads(field_object.state) return key.field_name in json.loads(field_object.state)
else: else:
return True return True
......
...@@ -165,7 +165,7 @@ class XModuleSettingsField(models.Model): ...@@ -165,7 +165,7 @@ class XModuleSettingsField(models.Model):
class XModuleStudentPrefsField(models.Model): class XModuleStudentPrefsField(models.Model):
""" """
Stores data set in the Scope.student_preferences scope by an xmodule field Stores data set in the Scope.preferences scope by an xmodule field
""" """
class Meta: class Meta:
...@@ -199,7 +199,7 @@ class XModuleStudentPrefsField(models.Model): ...@@ -199,7 +199,7 @@ class XModuleStudentPrefsField(models.Model):
class XModuleStudentInfoField(models.Model): class XModuleStudentInfoField(models.Model):
""" """
Stores data set in the Scope.student_preferences scope by an xmodule field Stores data set in the Scope.preferences scope by an xmodule field
""" """
class Meta: class Meta:
......
...@@ -177,18 +177,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -177,18 +177,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
# Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash.
ajax_url = ajax_url.rstrip('/') ajax_url = ajax_url.rstrip('/')
# Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format(
host=request.get_host(),
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
)
def make_xqueue_callback(dispatch='score_update'): def make_xqueue_callback(dispatch='score_update'):
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format( xqueue_callback_url = '{proto}://{host}'.format(
host=request.get_host(), host=request.get_host(),
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
) )
xqueue_callback_url = settings.XQUEUE_INTERFACE.get('callback_url',xqueue_callback_url) # allow override
xqueue_callback_url += reverse('xqueue_callback', xqueue_callback_url += reverse('xqueue_callback',
kwargs=dict(course_id=course_id, kwargs=dict(course_id=course_id,
......
...@@ -32,9 +32,9 @@ course_id = 'edX/test_course/test' ...@@ -32,9 +32,9 @@ course_id = 'edX/test_course/test'
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id')) content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id')) settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
student_state_key = partial(LmsKeyValueStore.Key, Scope.student_state, 'user', location('def_id')) user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem') prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None) user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
class UserFactory(factory.Factory): class UserFactory(factory.Factory):
...@@ -115,13 +115,13 @@ class TestInvalidScopes(TestCase): ...@@ -115,13 +115,13 @@ class TestInvalidScopes(TestCase):
def setUp(self): def setUp(self):
self.desc_md = {} self.desc_md = {}
self.user = UserFactory.create() self.user = UserFactory.create()
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_invalid_scopes(self): def test_invalid_scopes(self):
for scope in (Scope(student=True, block=BlockScope.DEFINITION), for scope in (Scope(user=True, block=BlockScope.DEFINITION),
Scope(student=False, block=BlockScope.TYPE), Scope(user=False, block=BlockScope.TYPE),
Scope(student=False, block=BlockScope.ALL)): Scope(user=False, block=BlockScope.ALL)):
self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field')) self.assertRaises(InvalidScopeError, self.kvs.get, LmsKeyValueStore.Key(scope, None, None, 'field'))
self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value') self.assertRaises(InvalidScopeError, self.kvs.set, LmsKeyValueStore.Key(scope, None, None, 'field'), 'value')
self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field')) self.assertRaises(InvalidScopeError, self.kvs.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
...@@ -134,48 +134,48 @@ class TestStudentModuleStorage(TestCase): ...@@ -134,48 +134,48 @@ class TestStudentModuleStorage(TestCase):
self.desc_md = {} self.desc_md = {}
student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'})) student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
self.user = student_module.student self.user = student_module.student
self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.student_state, 'a_field')])], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
def test_get_existing_field(self): def test_get_existing_field(self):
"Test that getting an existing field in an existing StudentModule works" "Test that getting an existing field in an existing StudentModule works"
self.assertEquals('a_value', self.kvs.get(student_state_key('a_field'))) self.assertEquals('a_value', self.kvs.get(user_state_key('a_field')))
def test_get_missing_field(self): def test_get_missing_field(self):
"Test that getting a missing field from an existing StudentModule raises a KeyError" "Test that getting a missing field from an existing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field')) self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
def test_set_existing_field(self): def test_set_existing_field(self):
"Test that setting an existing student_state field changes the value" "Test that setting an existing user_state field changes the value"
self.kvs.set(student_state_key('a_field'), 'new_value') self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state)) self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_set_missing_field(self): def test_set_missing_field(self):
"Test that setting a new student_state field changes the value" "Test that setting a new user_state field changes the value"
self.kvs.set(student_state_key('not_a_field'), 'new_value') self.kvs.set(user_state_key('not_a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state)) self.assertEquals({'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_delete_existing_field(self): def test_delete_existing_field(self):
"Test that deleting an existing field removes it from the StudentModule" "Test that deleting an existing field removes it from the StudentModule"
self.kvs.delete(student_state_key('a_field')) self.kvs.delete(user_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertRaises(KeyError, self.kvs.get, student_state_key('not_a_field')) self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
def test_delete_missing_field(self): def test_delete_missing_field(self):
"Test that deleting a missing field from an existing StudentModule raises a KeyError" "Test that deleting a missing field from an existing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.delete, student_state_key('not_a_field')) self.assertRaises(KeyError, self.kvs.delete, user_state_key('not_a_field'))
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state)) self.assertEquals({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_has_existing_field(self): def test_has_existing_field(self):
"Test that `has` returns True for existing fields in StudentModules" "Test that `has` returns True for existing fields in StudentModules"
self.assertTrue(self.kvs.has(student_state_key('a_field'))) self.assertTrue(self.kvs.has(user_state_key('a_field')))
def test_has_missing_field(self): def test_has_missing_field(self):
"Test that `has` returns False for missing fields in StudentModule" "Test that `has` returns False for missing fields in StudentModule"
self.assertFalse(self.kvs.has(student_state_key('not_a_field'))) self.assertFalse(self.kvs.has(user_state_key('not_a_field')))
class TestMissingStudentModule(TestCase): class TestMissingStudentModule(TestCase):
...@@ -187,14 +187,14 @@ class TestMissingStudentModule(TestCase): ...@@ -187,14 +187,14 @@ class TestMissingStudentModule(TestCase):
def test_get_field_from_missing_student_module(self): def test_get_field_from_missing_student_module(self):
"Test that getting a field from a missing StudentModule raises a KeyError" "Test that getting a field from a missing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.get, student_state_key('a_field')) self.assertRaises(KeyError, self.kvs.get, user_state_key('a_field'))
def test_set_field_in_missing_student_module(self): def test_set_field_in_missing_student_module(self):
"Test that setting a field in a missing StudentModule creates the student module" "Test that setting a field in a missing StudentModule creates the student module"
self.assertEquals(0, len(self.mdc.cache)) self.assertEquals(0, len(self.mdc.cache))
self.assertEquals(0, StudentModule.objects.all().count()) self.assertEquals(0, StudentModule.objects.all().count())
self.kvs.set(student_state_key('a_field'), 'a_value') self.kvs.set(user_state_key('a_field'), 'a_value')
self.assertEquals(1, len(self.mdc.cache)) self.assertEquals(1, len(self.mdc.cache))
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
...@@ -207,11 +207,11 @@ class TestMissingStudentModule(TestCase): ...@@ -207,11 +207,11 @@ class TestMissingStudentModule(TestCase):
def test_delete_field_from_missing_student_module(self): def test_delete_field_from_missing_student_module(self):
"Test that deleting a field from a missing StudentModule raises a KeyError" "Test that deleting a field from a missing StudentModule raises a KeyError"
self.assertRaises(KeyError, self.kvs.delete, student_state_key('a_field')) self.assertRaises(KeyError, self.kvs.delete, user_state_key('a_field'))
def test_has_field_for_missing_student_module(self): def test_has_field_for_missing_student_module(self):
"Test that `has` returns False for missing StudentModules" "Test that `has` returns False for missing StudentModules"
self.assertFalse(self.kvs.has(student_state_key('a_field'))) self.assertFalse(self.kvs.has(user_state_key('a_field')))
class StorageTestBase(object): class StorageTestBase(object):
...@@ -286,13 +286,13 @@ class TestContentStorage(StorageTestBase, TestCase): ...@@ -286,13 +286,13 @@ class TestContentStorage(StorageTestBase, TestCase):
class TestStudentPrefsStorage(StorageTestBase, TestCase): class TestStudentPrefsStorage(StorageTestBase, TestCase):
factory = StudentPrefsFactory factory = StudentPrefsFactory
scope = Scope.student_preferences scope = Scope.preferences
key_factory = student_prefs_key key_factory = prefs_key
storage_class = XModuleStudentPrefsField storage_class = XModuleStudentPrefsField
class TestStudentInfoStorage(StorageTestBase, TestCase): class TestStudentInfoStorage(StorageTestBase, TestCase):
factory = StudentInfoFactory factory = StudentInfoFactory
scope = Scope.student_info scope = Scope.user_info
key_factory = student_info_key key_factory = user_info_key
storage_class = XModuleStudentInfoField storage_class = XModuleStudentInfoField
import logging from mock import MagicMock
from mock import MagicMock, patch
import datetime import datetime
import factory
import unittest
import os
from django.test import TestCase from django.test import TestCase
from django.http import Http404, HttpResponse from django.http import Http404
from django.conf import settings from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import RequestFactory from django.test.client import RequestFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore, _MODULESTORES from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError,\
ItemNotFoundError, NoPathToItem
import courseware.views as views import courseware.views as views
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .factories import UserFactory
class Stub(): class Stub():
pass pass
...@@ -55,7 +48,6 @@ class TestJumpTo(TestCase): ...@@ -55,7 +48,6 @@ class TestJumpTo(TestCase):
def test_jumpto_invalid_location(self): def test_jumpto_invalid_location(self):
location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None) location = Location('i4x', 'edX', 'toy', 'NoSuchPlace', None)
jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location) jumpto_url = '%s/%s/jump_to/%s' % ('/courses', self.course_name, location)
expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
response = self.client.get(jumpto_url) response = self.client.get(jumpto_url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -124,3 +116,26 @@ class ViewsTestCase(TestCase): ...@@ -124,3 +116,26 @@ class ViewsTestCase(TestCase):
request, 'bar', ()) request, 'bar', ())
self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request, self.assertRaisesRegexp(Http404, 'No data*', views.jump_to, request,
'dummy', self.location) 'dummy', self.location)
def test_no_end_on_about_page(self):
# Toy course has no course end date or about/end_date blob
self.verify_end_date(self.course_id)
def test_no_end_about_blob(self):
# test_end has a course end date, no end_date HTML blob
self.verify_end_date("edX/test_end/2012_Fall", "Sep 17, 2015")
def test_about_blob_end_date(self):
# test_about_blob_end_date has both a course end date and an end_date HTML blob.
# HTML blob wins
self.verify_end_date("edX/test_about_blob_end_date/2012_Fall", "Learning never ends")
def verify_end_date(self, course_id, expected_end_text=None):
request = self.request_factory.get("foo")
request.user = self.user
result = views.course_about(request, course_id)
if expected_end_text is not None:
self.assertContains(result, "Classes End")
self.assertContains(result, expected_end_text)
else:
self.assertNotContains(result, "Classes End")
...@@ -630,6 +630,7 @@ def progress(request, course_id, student_id=None): ...@@ -630,6 +630,7 @@ def progress(request, course_id, student_id=None):
'courseware_summary': courseware_summary, 'courseware_summary': courseware_summary,
'grade_summary': grade_summary, 'grade_summary': grade_summary,
'staff_access': staff_access, 'staff_access': staff_access,
'student': student,
} }
context.update() context.update()
......
...@@ -6,7 +6,8 @@ Enrollments. ...@@ -6,7 +6,8 @@ Enrollments.
""" """
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment, assign_default_role from student.models import CourseEnrollment
from django_comment_client.models import assign_default_role
class Command(BaseCommand): class Command(BaseCommand):
......
...@@ -6,7 +6,8 @@ Enrollments. ...@@ -6,7 +6,8 @@ Enrollments.
""" """
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from student.models import CourseEnrollment, assign_default_role from student.models import CourseEnrollment
from django_comment_client.models import assign_default_role
class Command(BaseCommand): class Command(BaseCommand):
......
"""
Reload forum (comment client) users from existing users.
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
import comment_client as cc
class Command(BaseCommand):
help = 'Reload forum (comment client) users from existing users'
def adduser(self,user):
print user
try:
cc_user = cc.User.from_django_user(user)
cc_user.save()
except Exception as err:
print "update user info to discussion failed for user with id: %s" % user
def handle(self, *args, **options):
if len(args) != 0:
uset = [User.objects.get(username=x) for x in args]
else:
uset = User.objects.all()
for user in uset:
self.adduser(user)
\ No newline at end of file
...@@ -41,6 +41,7 @@ from xmodule.modulestore import Location ...@@ -41,6 +41,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location from xmodule.modulestore.search import path_to_location
import xmodule.graders as xmgraders
import track.views import track.views
from .offline_gradecalc import student_grades, offline_grades_available from .offline_gradecalc import student_grades, offline_grades_available
...@@ -208,6 +209,10 @@ def instructor_dashboard(request, course_id): ...@@ -208,6 +209,10 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id))
elif 'Dump description of graded assignments configuration' in action:
track.views.server_track(request, action, {}, page='idashboard')
msg += dump_grading_context(course)
elif "Reset student's attempts" in action or "Delete student state for problem" in action: elif "Reset student's attempts" in action or "Delete student state for problem" in action:
# get the form data # get the form data
unique_student_identifier = request.POST.get('unique_student_identifier', '') unique_student_identifier = request.POST.get('unique_student_identifier', '')
...@@ -1122,3 +1127,50 @@ def compute_course_stats(course): ...@@ -1122,3 +1127,50 @@ def compute_course_stats(course):
walk(course) walk(course)
stats = dict(counts) # number of each kind of module stats = dict(counts) # number of each kind of module
return stats return stats
def dump_grading_context(course):
'''
Dump information about course grading context (eg which problems are graded in what assignments)
Very useful for debugging grading_policy.json and policy.json
'''
msg = "-----------------------------------------------------------------------------\n"
msg += "Course grader:\n"
msg += '%s\n' % course.grader.__class__
graders = {}
if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader):
msg += '\n'
msg += "Graded sections:\n"
for subgrader, category, weight in course.grader.sections:
msg += " subgrader=%s, type=%s, category=%s, weight=%s\n" % (subgrader.__class__, subgrader.type, category, weight)
subgrader.index = 1
graders[subgrader.type] = subgrader
msg += "-----------------------------------------------------------------------------\n"
msg += "Listing grading context for course %s\n" % course.id
gc = course.grading_context
msg += "graded sections:\n"
msg += '%s\n' % gc['graded_sections'].keys()
for (gs, gsvals) in gc['graded_sections'].items():
msg += "--> Section %s:\n" % (gs)
for sec in gsvals:
s = sec['section_descriptor']
format = getattr(s, 'format', None)
aname = ''
if format in graders:
g = graders[format]
aname = '%s %02d' % (g.short_label, g.index)
g.index += 1
elif s.display_name in graders:
g = graders[s.display_name]
aname = '%s' % g.short_label
notes = ''
if getattr(s, 'score_by_attempt', False):
notes = ', score by attempt!'
msg += " %s (format=%s, Assignment=%s%s)\n" % (s.display_name, format, aname, notes)
msg += "all descriptors:\n"
msg += "length=%d\n" % len(gc['all_descriptors'])
msg = '<pre>%s</pre>' % msg.replace('<','&lt;')
return msg
...@@ -39,12 +39,14 @@ def getip(request): ...@@ -39,12 +39,14 @@ def getip(request):
def get_commit_id(course): def get_commit_id(course):
return course.metadata.get('GIT_COMMIT_ID', 'No commit id') #return course.metadata.get('GIT_COMMIT_ID', 'No commit id')
return getattr(course, 'GIT_COMMIT_ID', 'No commit id')
# getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id') # getattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID','No commit id')
def set_commit_id(course, commit_id): def set_commit_id(course, commit_id):
course.metadata['GIT_COMMIT_ID'] = commit_id #course.metadata['GIT_COMMIT_ID'] = commit_id
setattr(course, 'GIT_COMMIT_ID', commit_id)
# setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id) # setattr(def_ms.courses[reload_dir], 'GIT_COMMIT_ID', new_commit_id)
...@@ -124,7 +126,8 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): ...@@ -124,7 +126,8 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
#---------------------------------------- #----------------------------------------
dumpfields = ['definition', 'location', 'metadata'] #dumpfields = ['definition', 'location', 'metadata']
dumpfields = ['location', 'metadata']
for cdir, course in def_ms.courses.items(): for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>' html += '<hr width="100%"/>'
...@@ -133,7 +136,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): ...@@ -133,7 +136,7 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
html += '<p>commit_id=%s</p>' % get_commit_id(course) html += '<p>commit_id=%s</p>' % get_commit_id(course)
for field in dumpfields: for field in dumpfields:
data = getattr(course, field) data = getattr(course, field, None)
html += '<h3>%s</h3>' % field html += '<h3>%s</h3>' % field
if type(data) == dict: if type(data) == dict:
html += '<ul>' html += '<ul>'
......
...@@ -15,7 +15,6 @@ from scipy.optimize import curve_fit ...@@ -15,7 +15,6 @@ from scipy.optimize import curve_fit
from django.conf import settings from django.conf import settings
from django.db.models import Sum, Max from django.db.models import Sum, Max
from psychometrics.models import * from psychometrics.models import *
from xmodule.modulestore import Location
log = logging.getLogger("mitx.psychometrics") log = logging.getLogger("mitx.psychometrics")
...@@ -246,6 +245,7 @@ def generate_plots_for_problem(problem): ...@@ -246,6 +245,7 @@ def generate_plots_for_problem(problem):
yset['ydat'] = ydat yset['ydat'] = ydat
if len(ydat) > 3: # try to fit to logistic function if enough data points if len(ydat) > 3: # try to fit to logistic function if enough data points
try:
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0]) cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitparam'] = cfp yset['fitparam'] = cfp
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0]) yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
...@@ -253,6 +253,8 @@ def generate_plots_for_problem(problem): ...@@ -253,6 +253,8 @@ def generate_plots_for_problem(problem):
fitx = np.linspace(xdat[0], xdat[-1], 100) fitx = np.linspace(xdat[0], xdat[-1], 100)
yset['fitx'] = fitx yset['fitx'] = fitx
yset['fity'] = func_2pl(np.array(fitx), *cfp[0]) yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
except Exception as err:
log.debug('Error in psychoanalyze curve fitting: %s' % err)
dataset['grade_%d' % grade] = yset dataset['grade_%d' % grade] = yset
...@@ -302,7 +304,7 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key): ...@@ -302,7 +304,7 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
Construct and return a procedure which may be called to update Construct and return a procedure which may be called to update
the PsychometricsData instance for the given StudentModule instance. the PsychometricsData instance for the given StudentModule instance.
""" """
sm = studentmodule.objects.get_or_create( sm, status = StudentModule.objects.get_or_create(
course_id=course_id, course_id=course_id,
student=user, student=user,
module_state_key=module_state_key, module_state_key=module_state_key,
...@@ -329,7 +331,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key): ...@@ -329,7 +331,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
return return
pmd.done = done pmd.done = done
pmd.attempts = state['attempts'] try:
pmd.attempts = state.get('attempts', 0)
except:
log.exception("no attempts for %s (state=%s)" % (sm, sm.state))
try: try:
checktimes = eval(pmd.checktimes) # update log of attempt timestamps checktimes = eval(pmd.checktimes) # update log of attempt timestamps
except: except:
......
...@@ -144,9 +144,19 @@ ...@@ -144,9 +144,19 @@
<li><div class="icon course-number"></div><p>Course Number</p><span class="course-number">${course.number}</span></li> <li><div class="icon course-number"></div><p>Course Number</p><span class="course-number">${course.number}</span></li>
<li><div class="icon start"></div><p>Classes Start</p><span class="start-date">${course.start_date_text}</span></li> <li><div class="icon start"></div><p>Classes Start</p><span class="start-date">${course.start_date_text}</span></li>
## End date should come from course.xml, but this is a quick hack ## We plan to ditch end_date (which is not stored in course metadata),
## but for backwards compatibility, show about/end_date blob if it exists.
% if get_course_about_section(course, "end_date") or course.end:
<li>
<div class="icon end"></div>
<p>Classes End</p><span class="final-date">
% if get_course_about_section(course, "end_date"): % if get_course_about_section(course, "end_date"):
<li><div class="icon end"></div><p>Classes End</p><span class="final-date">${get_course_about_section(course, "end_date")}</span></li> ${get_course_about_section(course, "end_date")}
% else:
${course.end_date_text}
% endif
</span>
</li>
% endif % endif
% if get_course_about_section(course, "effort"): % if get_course_about_section(course, "effort"):
......
...@@ -156,6 +156,7 @@ function goto( mode) ...@@ -156,6 +156,7 @@ function goto( mode)
<p> <p>
<input type="submit" name="action" value="Download CSV of answer distributions"> <input type="submit" name="action" value="Download CSV of answer distributions">
<input type="submit" name="action" value="Dump description of graded assignments configuration">
</p> </p>
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
......
...@@ -31,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", ...@@ -31,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<section class="course-info"> <section class="course-info">
<header> <header>
<h1>Course Progress</h1> <h1>Course Progress for Student '${student.username}' (${student.email})</h1>
</header> </header>
%if not course.disable_progress_graph: %if not course.disable_progress_graph:
......
...@@ -6,7 +6,16 @@ ...@@ -6,7 +6,16 @@
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/> <link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/> <link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
<title>EdX Blog</title> <title>EdX Blog</title>
<updated>2013-03-15T14:00:12-07:00</updated> <updated>2013-04-03T14:00:12-07:00</updated>
<entry>
<id>tag:www.edx.org,2012:Post/17</id>
<published>2012-12-19T14:00:00-07:00</published>
<updated>2012-12-19T14:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/stanford-to-work-with-edx')}"/>
<title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/releases/stanford-university_204x114.png')}&quot; /&gt;
&lt;p&gt;&lt;/p&gt;</content>
</entry>
<entry> <entry>
<id>tag:www.edx.org,2013:Post/16</id> <id>tag:www.edx.org,2013:Post/16</id>
<published>2013-03-15T10:00:00-07:00</published> <published>2013-03-15T10:00:00-07:00</published>
......
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../../main.html" />
<%namespace name='static' file='../../static_content.html'/>
<%block name="title"><title>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</title></%block>
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<section class="pressrelease">
<section class="container">
<h1>Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform</h1>
<hr class="horizontal-divider">
<article>
<h2>edX Learning Platform to be open source and available on June 1</h2>
<p><strong>CAMBRIDGE, MA and STANFORD, CA &ndash; April 3, 2013 &ndash;</strong>
Stanford University and <a href="https://www.edx.org">edX</a>, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced their collaboration to advance the development of edX’s open source learning platform and provide free and open online learning tools for institutions around the world.</p>
<p>As part of this announcement, edX will release the source code for its entire online learning platform on June 1, 2013. In support of that move, Stanford will integrate features of its existing Class2Go platform into the edX platform, use the integration as an internal platform for online coursework for on-campus and distance learners, and work collaboratively with edX and other institutions to further develop the edX platform.</p>
<p>“This collaboration brings together two leaders in online education in a common effort to ensure that the world’s universities have the strongest possible not-for-profit, open source platform available to them,” said John Mitchell, vice provost for online learning at Stanford University. “A not-for-profit, open source platform will help universities experiment with different ways to produce and share content, fostering continued innovation through a vibrant community of contributors.”</p>
<p>EdX and Stanford will collaborate along with others around the globe on the ongoing development and refinement of the edX online learning platform. As of June 1, developers everywhere will be able to freely access the source code of the edX learning platform, including code for its Learning Management System (LMS); Studio, a course authoring tool; xBlock, an application programming interface (API) for integrating third-party learning objects; and machine grading API’s. EdX will support and nurture the community of developers contributing to the enhancement of the edX platform by providing a rich environment for developer collaboration as well as technical and process guidelines to facilitate developer contributions.</p>
<p>“It has been our vision to offer our platform as open source since edX’s founding by Harvard and MIT,” stated Anant Agarwal, president of edX. “We are now realizing that vision, and I am pleased to welcome Stanford University, one of the world’s leading institutions of higher education, to further this global open source solution. I want to acknowledge the key role played by our X Consortium member UC Berkeley, which was instrumental in fostering this collaboration. We believe the edX platform—the Linux of learning—will benefit from all the world’s institutions and communities.”</p>
<p>EdX is pursuing an open source vision to enhance access to higher education for the entire world. One of the chief benefits of massive open online courses (MOOCs) is that they bring together a tremendously diverse student body to learn with and from each other. EdX has chosen to extend that perspective to its learning platform as well, knowing that drawing upon the global community of developers is an effective route to both transform and deliver the world’s best and most accessible online and blended learning experience.</p>
<p>MOOCs and innovative online teaching approaches on college campuses, such as the “flipped classroom,” use web environments that support interactive video, online discussion, social/cohort interaction, assessment and other functions. Open source online learning platforms will allow universities to develop their own delivery methods, partner with other universities and institutions as they choose, collect data, and control branding of their educational material. Further developing online opportunities through open source technology is a key objective of the partnership between edX and Stanford.</p>
<p>Stanford will continue to provide a range of platforms for its instructors to choose from in hosting their online coursework, including continued partnerships with Coursera and other providers. The university will focus its ongoing platform development efforts on the new platform, combining key features from the Class2Go open source platform with the open source edX code base.</p>
<p>The edX learning platform source code, as well as platform developments from Stanford, edX and other contributors, will be available on June 1, 2013 and can be accessed from the edX Platform Repository located at <a href="https://github.com/edX">https://github.com/edX</a>.</p>
<h2>About edX</h2>
<p><a href="https://www.edx.org/">EdX</a> is a not-for-profit enterprise of its founding partners <a href="http://www.harvard.edu">Harvard University</a> and the <a href="http://www.mit.edu">Massachusetts Institute of Technology</a> focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.</p>
<h2>About Stanford University</h2>
<p>
<a href="http://www.stanford.edu">Stanford University</a> is engaged in a variety of efforts to develop online learning – experimenting with coursework for both on-campus and off-campus students, researching key questions around what a digital environment means for teaching and learning, and pursuing platform development. More information on Stanford’s online learning activities is available at <a href="http://online.stanford.edu">http://online.stanford.edu</a>
<section class="contact">
<p><strong>Media Contact:</strong></p>
<p>Dan O'Connell</p>
<p>oconnell@edx.org</p>
<p>(617) 480-6585</p>
</section>
<section class="contact">
<p>Brad Hayward</p>
<p>bhayward@stanford.edu</p>
<p>650-724-0199</p>
</section>
<section class="contact">
<p>Lisa Lapin</p>
<p>lapin@stanford.edu</p>
<p>650-725-8396</p>
</section>
<section class="footer">
<hr class="horizontal-divider">
<div class="logo"></div><h3 class="date">DATE: 04 - 03 - 2013</h3>
<div class="social-sharing">
<hr class="horizontal-divider">
<p>Share with friends and family:</p>
<a href="http://twitter.com/intent/tweet?text=:Stanford+to+work+with+edX+http://www.edx.org/press/stanford-to-work-with-edx" class="share">
<img src="${static.url('images/social/twitter-sharing.png')}">
</a>
</a>
<a href="mailto:?subject=Stanford%20to%20work%20with%20EdX…http://edx.org/press/stanford-to-work-with-edx" class="share">
<img src="${static.url('images/social/email-sharing.png')}">
</a>
<div class="fb-like" data-href="http://edx.org/press/stanford-to-work-with-edx" data-send="true" data-width="450" data-show-faces="true"></div>
</div>
</section>
</article>
</section>
</section>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
</%block> </%block>
<%block name="university_description"> <%block name="university_description">
<p>The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools, and is the first major American university to build a medical school in the past 50 years.</p> <p>The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools. UT Austin will be opening the Dell Medical School in 2016.</p>
</%block> </%block>
${parent.body()} ${parent.body()}
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<%block name="university_description"> <%block name="university_description">
<p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p> <p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p>
<p>Find out about the <a href="${reverse('university_profile', args=['UTAustinX'])}">University of Texas Austin</a>.</p> <p>Find out about <a href="${reverse('university_profile', args=['UTAustinX'])}">The University of Texas at Austin</a>.</p>
</%block> </%block>
${parent.body()} ${parent.body()}
...@@ -153,6 +153,9 @@ urlpatterns = ('', ...@@ -153,6 +153,9 @@ urlpatterns = ('',
url(r'^press/xblock_announcement$', 'static_template_view.views.render', url(r'^press/xblock_announcement$', 'static_template_view.views.render',
{'template': 'press_releases/xblock_announcement.html'}, {'template': 'press_releases/xblock_announcement.html'},
name="press/xblock-announcement"), name="press/xblock-announcement"),
url(r'^press/stanford-to-work-with-edx$', 'static_template_view.views.render',
{'template': 'press_releases/stanford_announcement.html'},
name="press/stanford-to-work-with-edx"),
# Should this always update to point to the latest press release? # Should this always update to point to the latest press release?
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', (r'^pressrelease$', 'django.views.generic.simple.redirect_to',
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
# XBlock: # XBlock:
# Might change frequently, so put it in local-requirements.txt, # Might change frequently, so put it in local-requirements.txt,
# but conceptually is an external package, so it is in a separate repo. # but conceptually is an external package, so it is in a separate repo.
-e git+ssh://git@github.com/MITx/xmodule-debugger@9a4f883a#egg=XBlock -e git+https://github.com/edx/XBlock.git@96d8f5f4#egg=XBlock
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