Commit 2e852d16 by ichuang

Merge remote-tracking branch 'edx-mitx/master' into feature/ichuang/staff-email-for-course-creation

Conflicts:
	cms/envs/common.py
parents 1969a663 72ce1af9
...@@ -34,6 +34,7 @@ load-plugins= ...@@ -34,6 +34,7 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where # multiple time (only on the command line, not in the configuration file where
# it should appear only once). # it should appear only once).
disable= disable=
# C0301: Line too long
# W0141: Used builtin function 'map' # W0141: Used builtin function 'map'
# W0142: Used * or ** magic # W0142: Used * or ** magic
# R0201: Method could be a function # R0201: Method could be a function
...@@ -42,7 +43,7 @@ disable= ...@@ -42,7 +43,7 @@ disable=
# R0903: Too few public methods (1/2) # R0903: Too few public methods (1/2)
# R0904: Too many public methods # R0904: Too many public methods
# R0913: Too many arguments # R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS] [REPORTS]
......
...@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore ...@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'): ...@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'):
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
world.css_fill(name_css, name) world.css_fill(name_css, name)
world.css_click(save_css) world.css_click(save_css)
def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(date_css, desired_date)
# hit TAB to get to the time field
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS #################### ############### ACTIONS ####################
...@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step): ...@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step):
@step('I save a new section release date$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
time_css = 'input.start-time.time.ui-timepicker-input' 'input.start-time.time.ui-timepicker-input', '12:00am')
world.css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, '12:00am')
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save') world.browser.click_link_by_text('Save')
......
...@@ -25,6 +25,13 @@ Feature: Create Subsection ...@@ -25,6 +25,13 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see it marked as Homework Then I see it marked as Homework
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
Then I see the correct dates
And I reload the page
Then I see the correct dates
@skip-phantom @skip-phantom
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
...@@ -33,3 +40,5 @@ Feature: Create Subsection ...@@ -33,3 +40,5 @@ Feature: Create Subsection
When I press the "subsection" delete icon When I press the "subsection" delete icon
And I confirm the alert And I confirm the alert
Then the subsection does not exist Then the subsection does not exist
...@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step): ...@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step):
add_section() add_section()
@step('I have added a new subsection$')
def i_have_added_a_new_subsection(step):
add_subsection()
@step('I have opened a new subsection in Studio$')
def i_have_opened_a_new_subsection(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
world.css_click('span.subsection-name-value')
@step('I click the New Subsection link') @step('I click the New Subsection link')
def i_click_the_new_subsection_link(step): def i_click_the_new_subsection_link(step):
world.css_click('a.new-subsection-item') world.css_click('a.new-subsection-item')
...@@ -43,9 +55,20 @@ def i_see_complete_subsection_name_with_quote_in_editor(step): ...@@ -43,9 +55,20 @@ def i_see_complete_subsection_name_with_quote_in_editor(step):
assert_equal(world.css_find(css).value, 'Subsection With "Quote"') assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$') @step('I have set a release date and due date in different years$')
def i_have_added_a_new_subsection(step): def test_have_set_dates_in_different_years(step):
add_subsection() set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '3:00am')
world.css_click('.set-date')
# Use a year in the past so that current year will always be different.
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '4:00am')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('3:00am', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('4:00am', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$') @step('I mark it as Homework$')
......
...@@ -151,10 +151,6 @@ def compute_unit_state(unit): ...@@ -151,10 +151,6 @@ def compute_unit_state(unit):
return UnitState.public return UnitState.public
def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value): def update_item(location, value):
""" """
If value is None, delete the db entry. Otherwise, update it using the correct modulestore. If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
......
...@@ -6,7 +6,6 @@ import sys ...@@ -6,7 +6,6 @@ import sys
import time import time
import tarfile import tarfile
import shutil import shutil
from datetime import datetime
from collections import defaultdict from collections import defaultdict
from uuid import uuid4 from uuid import uuid4
from path import path from path import path
...@@ -42,17 +41,18 @@ from xmodule.modulestore.mongo import MongoUsage ...@@ -42,17 +41,18 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from functools import partial from functools import partial
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \ from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
get_date_display, UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \ UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
remove_open_ended_panel_tab remove_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
...@@ -365,7 +365,7 @@ def edit_unit(request, location): ...@@ -365,7 +365,7 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None, 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section, 'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state, 'unit_state': unit_state,
...@@ -439,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -439,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
...@@ -821,7 +828,7 @@ def upload_asset(request, org, course, coursename): ...@@ -821,7 +828,7 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = {'displayname': content.name, response_payload = {'displayname': content.name,
'uploadDate': get_date_display(readback.last_modified_at), 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location), 'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed' 'msg': 'Upload completed'
...@@ -1426,7 +1433,7 @@ def asset_index(request, org, course, name): ...@@ -1426,7 +1433,7 @@ def asset_index(request, org, course, name):
id = asset['_id'] id = asset['_id']
display_info = {} display_info = {}
display_info['displayname'] = asset['displayname'] display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_date_display(asset['uploadDate']) display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name']) asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location) display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
......
...@@ -35,6 +35,7 @@ MITX_FEATURES = { ...@@ -35,6 +35,7 @@ MITX_FEATURES = {
'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) 'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0 ...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################ DEBUG TOOLBAR ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
...@@ -143,3 +147,6 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -143,3 +147,6 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = True DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
...@@ -4,6 +4,9 @@ var $modalCover; ...@@ -4,6 +4,9 @@ var $modalCover;
var $newComponentItem; var $newComponentItem;
var $changedInput; var $changedInput;
var $spinner; var $spinner;
var $newComponentTypePicker;
var $newComponentTemplatePickers;
var $newComponentButton;
$(document).ready(function () { $(document).ready(function () {
$body = $('body'); $body = $('body');
...@@ -83,6 +86,8 @@ $(document).ready(function () { ...@@ -83,6 +86,8 @@ $(document).ready(function () {
// general link management - smooth scrolling page links // general link management - smooth scrolling page links
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
// toggling overview section details // toggling overview section details
$(function () { $(function () {
...@@ -160,6 +165,18 @@ function smoothScrollLink(e) { ...@@ -160,6 +165,18 @@ function smoothScrollLink(e) {
}); });
} }
function smoothScrollTop(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $('#view-top')
});
}
function linkNewWindow(e) { function linkNewWindow(e) {
window.open($(e.target).attr('href')); window.open($(e.target).attr('href'));
e.preventDefault(); e.preventDefault();
...@@ -228,7 +245,7 @@ function syncReleaseDate(e) { ...@@ -228,7 +245,7 @@ function syncReleaseDate(e) {
$("#start_time").val(""); $("#start_time").val("");
} }
function getEdxTimeFromDateTimeVals(date_val, time_val, format) { function getEdxTimeFromDateTimeVals(date_val, time_val) {
var edxTimeStr = null; var edxTimeStr = null;
if (date_val != '') { if (date_val != '') {
...@@ -237,20 +254,17 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) { ...@@ -237,20 +254,17 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
var date = Date.parse(date_val + " " + time_val); var date = Date.parse(date_val + " " + time_val);
if (format == null) edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
format = 'yyyy-MM-ddTHH:mm';
edxTimeStr = date.toString(format);
} }
return edxTimeStr; return edxTimeStr;
} }
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { function getEdxTimeFromDateTimeInputs(date_id, time_id) {
var input_date = $('#' + date_id).val(); var input_date = $('#' + date_id).val();
var input_time = $('#' + time_id).val(); var input_time = $('#' + time_id).val();
return getEdxTimeFromDateTimeVals(input_date, input_time, format); return getEdxTimeFromDateTimeVals(input_date, input_time);
} }
function autosaveInput(e) { function autosaveInput(e) {
...@@ -291,10 +305,8 @@ function saveSubsection() { ...@@ -291,10 +305,8 @@ function saveSubsection() {
} }
// Piece back together the date/time UI elements into one date/time string // Piece back together the date/time UI elements into one date/time string
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
// so make sure we're passing back the correct format
metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time'); metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm'); metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time');
$.ajax({ $.ajax({
url: "/save_item", url: "/save_item",
...@@ -316,8 +328,8 @@ function saveSubsection() { ...@@ -316,8 +328,8 @@ function saveSubsection() {
function createNewUnit(e) { function createNewUnit(e) {
e.preventDefault(); e.preventDefault();
parent = $(this).data('parent'); var parent = $(this).data('parent');
template = $(this).data('template'); var template = $(this).data('template');
$.post('/clone_item', $.post('/clone_item',
{'parent_location': parent, {'parent_location': parent,
......
...@@ -644,7 +644,7 @@ hr.divide { ...@@ -644,7 +644,7 @@ hr.divide {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99999; z-index: 10000;
padding: 0 10px; padding: 0 10px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
......
...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125); ...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125);
$black-t1: rgba(0,0,0,0.25); $black-t1: rgba(0,0,0,0.25);
$black-t2: rgba(0,0,0,0.50); $black-t2: rgba(0,0,0,0.50);
$black-t3: rgba(0,0,0,0.75); $black-t3: rgba(0,0,0,0.75);
$white: rgb(255,255,255); $white: rgb(255,255,255);
$white-t0: rgba(255,255,255,0.125); $white-t0: rgba(255,255,255,0.125);
$white-t1: rgba(255,255,255,0.25); $white-t1: rgba(255,255,255,0.25);
......
...@@ -27,7 +27,8 @@ ...@@ -27,7 +27,8 @@
@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';
// specific views // specific views
@import 'views/account'; @import 'views/account';
......
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
// specific elements - course nav // specific elements - course nav
.nav-course { .nav-course {
width: 335px; width: 285px;
margin-top: -($baseline/4); margin-top: -($baseline/4);
@include font-size(14); @include font-size(14);
......
// tender help/support widget
// ====================
#tender_frame, #tender_window {
background-image: none !important;
background: none;
}
#tender_window {
@include border-radius(3px);
@include box-shadow(0 2px 3px $shadow);
height: ($baseline*35) !important;
background: $white !important;
border: 1px solid $gray;
}
#tender_window {
padding: 0 !important;
}
#tender_frame {
background: $white;
}
#tender_closer {
color: $blue-l2 !important;
text-transform: uppercase;
&:hover {
color: $blue-l4 !important;
}
}
// ====================
// tender style overrides - not rendered through here, but an archive is needed
#tender_frame iframe html {
font-size: 62.5%;
}
.widget-layout {
font-family: 'Open Sans', sans-serif;
}
.widget-layout .search,
.widget-layout .tabs,
.widget-layout .footer,
.widget-layout .header h1 a {
display: none;
}
.widget-layout .header {
background: rgb(85, 151, 221);
padding: 10px 20px;
}
.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label {
font-weight: 600;
}
.widget-layout .header h1 {
font-size: 22px;
}
.widget-layout .content {
overflow: auto;
height: auto !important;
padding: 20px;
}
.widget-layout .flash {
margin: -10px 0 15px 0;
padding: 10px 20px !important;
background-image: none !important;
}
.widget-layout .flash-error {
background: rgb(178, 6, 16) !important;
color: rgb(255,255,255) !important;
}
.widget-layout label {
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout input[type="text"], .widget-layout textarea {
padding: 10px;
font-size: 16px;
color: rgb(0,0,0) !important;
border: 1px solid #b0b6c2;
border-radius: 2px;
background-color: #edf1f5;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #edf1f5),color-stop(100%, #fdfdfe));
background-image: -webkit-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -moz-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -ms-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -o-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: linear-gradient(top, #edf1f5,#fdfdfe);
background-color: #edf1f5;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
}
.widget-layout input[type="text"]:focus, .widget-layout textarea:focus {
background-color: #fffcf1;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fffcf1),color-stop(100%, #fffefd));
background-image: -webkit-linear-gradient(top, #fffcf1,#fffefd);
background-image: -moz-linear-gradient(top, #fffcf1,#fffefd);
background-image: -ms-linear-gradient(top, #fffcf1,#fffefd);
background-image: -o-linear-gradient(top, #fffcf1,#fffefd);
background-image: linear-gradient(top, #fffcf1,#fffefd);
outline: 0;
}
.widget-layout textarea {
width: 97%;
}
.widget-layout p.note {
text-align: right !important;
display: inline-block !important;
position: absolute !important;
right: -130px !important;
top: -5px !important;
font-size: 13px !important;
opacity: 0.80;
}
.widget-layout .form-actions {
margin: 15px 0;
border: none;
padding: 0;
}
.widget-layout dl.form {
float: none;
width: 100%;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 10px;
padding-bottom: 10px;
}
.widget-layout dl.form:last-child {
border: none;
padding-bottom: 0;
margin-bottom: 20px;
}
.widget-layout dl.form dt, .widget-layout dl.form dd {
display: inline-block;
vertical-align: middle;
}
.widget-layout dl.form dt {
margin-right: 15px;
width: 70px;
}
.widget-layout dl.form dd {
width: 65%;
position: relative;
}
// specific elements
.widget-layout #discussion_body {
}
.widget-layout #discussion_body:before {
content: "What Question or Feedback Would You Like to Share?";
display: block;
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout dl#brain_buster_captcha {
float: none;
width: 100%;
border-top: 1px solid #f2f2f2;
margin-top: 10px;
padding-top: 10px;
}
.widget-layout dl#brain_buster_captcha dd {
display: block !important;
}
.widget-layout dl#brain_buster_captcha #captcha_answer {
border-color: #333;
}
.widget-layout dl#brain_buster_captcha dd label {
display: block;
font-weight: 700;
margin: 0 15px 5px 0 !important;
}
.widget-layout dl#brain_buster_captcha dd #captcha_answer {
display: block;
width: 97%%;
}
.widget-layout .form-actions .btn-post_topic {
display: block;
width: 100%;
height: auto !important;
font-size: 16px;
font-weight: 700;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-webkit-transition-property: background-color,0.15s;
-moz-transition-property: background-color,0.15s;
-ms-transition-property: background-color,0.15s;
-o-transition-property: background-color,0.15s;
transition-property: background-color,0.15s;
-webkit-transition-duration: box-shadow,0.15s;
-moz-transition-duration: box-shadow,0.15s;
-ms-transition-duration: box-shadow,0.15s;
-o-transition-duration: box-shadow,0.15s;
transition-duration: box-shadow,0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0;
border: 1px solid #34854c;
border-radius: 3px;
background-color: rgba(255,255,255,0.3);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.3)),color-stop(100%, rgba(255,255,255,0)));
background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -moz-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -ms-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -o-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-color: #25b85a;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
color: #fff;
text-align: center;
margin-top: 20px;
padding: 10px 20px;
}
.widget-layout .form-actions #private-discussion-opt {
float: none;
text-align: left;
margin: 0 0 15px 0;
}
.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active {
background-color: #16ca57;
color: #fff;
}
\ No newline at end of file
// 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;
...@@ -8,6 +9,7 @@ ...@@ -8,6 +9,7 @@
font-family: $sans-serif; font-family: $sans-serif;
font-size: 12px; font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important;
.ui-widget-header { .ui-widget-header {
background: $darkGrey; background: $darkGrey;
...@@ -53,4 +55,11 @@ ...@@ -53,4 +55,11 @@
border-color: $orange; border-color: $orange;
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: 265px;
margin-right: 220px;
@include border-radius(3px); @include border-radius(3px);
background: $lightGrey; background: $lightGrey;
......
...@@ -55,9 +55,11 @@ ...@@ -55,9 +55,11 @@
<%block name="content"></%block> <%block name="content"></%block>
<%include file="widgets/footer.html" /> <%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
<%include file="widgets/qualaroo.html" />
</html> </html>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
from time import mktime
import dateutil.parser
import logging import logging
from datetime import datetime from xmodule.util.date_utils import get_time_struct_display
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
...@@ -13,7 +11,6 @@ ...@@ -13,7 +11,6 @@
<%namespace name="units" file="widgets/units.html" /> <%namespace name="units" file="widgets/units.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%namespace name='datetime' module='datetime'/>
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
...@@ -38,18 +35,15 @@ ...@@ -38,18 +35,15 @@
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label> <label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript"> <div class="datepair" data-language="javascript">
<% <input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None <input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start: % if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None: % if parent_item.lms.start is None:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset. <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else: % else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %I:%M %p')}.
% endif % endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p> <a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif % endif
...@@ -66,12 +60,8 @@ ...@@ -66,12 +60,8 @@
<a href="#" class="set-date">Set a due date</a> <a href="#" class="set-date">Set a due date</a>
<div class="datepair date-setter"> <div class="datepair date-setter">
<p class="date-description"> <p class="date-description">
<% <input type="text" id="due_date" name="due_date" value="${get_time_struct_display(subsection.lms.due, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use <input type="text" id="due_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
%>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a> <a href="#" class="remove-date">Remove due date</a>
</p> </p>
</div> </div>
......
...@@ -69,7 +69,7 @@ ...@@ -69,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>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
from time import mktime
import dateutil.parser
import logging import logging
from datetime import datetime from xmodule.util.date_utils import get_time_struct_display
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Outline</%block> <%block name="title">Course Outline</%block>
...@@ -163,11 +161,10 @@ ...@@ -163,11 +161,10 @@
</h3> </h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else '' start_time_str = get_time_struct_display(section.lms.start, '%I:%M %p')
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%> %>
%if start_date is None: %if section.lms.start is None:
<span class="published-status">This section has not been released.</span> <span class="published-status">This section has not been released.</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
%else: %else:
......
...@@ -14,15 +14,14 @@ ...@@ -14,15 +14,14 @@
<li class="nav-item nav-peripheral-pp"> <li class="nav-item nav-peripheral-pp">
<a href="#">Privacy Policy</a> <a href="#">Privacy Policy</a>
</li> --> </li> -->
<li class="nav-item nav-peripheral-help"> <li class="nav-item nav-peripheral-help">
<a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a> <a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a>
</li> </li>
<li class="nav-item nav-peripheral-contact">
<a href="https://www.edx.org/contact" rel="external">Contact edX</a>
</li>
% if user.is_authenticated(): % if user.is_authenticated():
<!-- add in zendesk/tender feedback form UI --> <li class="nav-item nav-peripheral-feedback">
<a class="show-tender" href="http://help.edge.edx.org/discussion/new" title="Use our feedback tool, Tender, to share your feedback">Contact Us</a>
</li>
% endif % endif
</ol> </ol>
</nav> </nav>
......
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<div class="wrapper-header wrapper"> <div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner"> <header class="primary" role="banner">
<div class="wrapper wrapper-left "> <div class="wrapper wrapper-left ">
......
% if settings.MITX_FEATURES.get('STUDIO_NPS_SURVEY'):
<!-- Qualaroo is used for net promoter score surveys -->
<script type="text/javascript">
% if user.is_authenticated():
var _kiq = _kiq || [];
_kiq.push(['identify', "${ user.email }" ]);
% endif
</script>
<!-- Qualaroo for edx.org -->
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/48221/9SN.js" async="true"></script>
<!-- end Qualaroo -->
% endif
% if user.is_authenticated():
<script type="text/javascript">
Tender = {
hideToggle: true,
title: '',
body: '',
hide_kb: 'true',
widgetToggles: $('.show-tender')
}
</script>
<script src="https://edxedge.tenderapp.com/tender_widget.js" type="text/javascript"></script>
% endif
\ No newline at end of file
#pylint: disable=C0111 #pylint: disable=C0111
#pylint: disable=W0621 #pylint: disable=W0621
# Disable the "wildcard import" warning so we can bring in all methods from
# course helpers and ui helpers
#pylint: disable=W0401
# Disable the "Unused import %s from wildcard import" warning
#pylint: disable=W0614
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613
from lettuce import world, step from lettuce import world, step
from .course_helpers import * from .course_helpers import *
from .ui_helpers import * from .ui_helpers import *
from lettuce.django import django_url from lettuce.django import django_url
from nose.tools import assert_equals, assert_in from nose.tools import assert_equals, assert_in
import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -125,11 +134,6 @@ def i_am_logged_in(step): ...@@ -125,11 +134,6 @@ def i_am_logged_in(step):
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
@step(u'I am an edX user$') @step(u'I am an edX user$')
def i_am_an_edx_user(step): def i_am_an_edx_user(step):
world.create_user('robot') world.create_user('robot')
......
...@@ -32,6 +32,8 @@ from copy import deepcopy ...@@ -32,6 +32,8 @@ from copy import deepcopy
import chem import chem
import chem.miller import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers import verifiers
import verifiers.draganddrop import verifiers.draganddrop
...@@ -67,6 +69,9 @@ global_context = {'random': random, ...@@ -67,6 +69,9 @@ global_context = {'random': random,
'scipy': scipy, 'scipy': scipy,
'calc': calc, 'calc': calc,
'eia': eia, 'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop} 'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
......
...@@ -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
...@@ -1140,12 +1140,13 @@ registry.register(DesignProtein2dInput) ...@@ -1140,12 +1140,13 @@ registry.register(DesignProtein2dInput)
class EditAGeneInput(InputTypeBase): class EditAGeneInput(InputTypeBase):
""" """
An input type for editing a gene. Integrates with the genex java applet. An input type for editing a gene.
Integrates with the genex GWT application.
Example: Example:
<editagene width="800" hight="500" dna_sequence="ETAAGGCTATAACCGA" /> <editagene genex_dna_sequence="CGAT" genex_problem_number="1"/>
""" """
template = "editageneinput.html" template = "editageneinput.html"
tags = ['editageneinput'] tags = ['editageneinput']
...@@ -1155,9 +1156,7 @@ class EditAGeneInput(InputTypeBase): ...@@ -1155,9 +1156,7 @@ class EditAGeneInput(InputTypeBase):
""" """
Note: width, height, and dna_sequencee are required. Note: width, height, and dna_sequencee are required.
""" """
return [Attribute('width'), return [Attribute('genex_dna_sequence'),
Attribute('height'),
Attribute('dna_sequence'),
Attribute('genex_problem_number') Attribute('genex_problem_number')
] ]
......
...@@ -17,6 +17,7 @@ import logging ...@@ -17,6 +17,7 @@ import logging
import numbers import numbers
import numpy import numpy
import os import os
import sys
import random import random
import re import re
import requests import requests
...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception): ...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(Exception): class ResponseError(Exception):
''' '''
Error for failure in processing a response Error for failure in processing a response, including
exceptions that occur when executing a custom script.
''' '''
pass pass
class StudentInputError(Exception): class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse): ...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys import sys
type, value, traceback = sys.exc_info() type, value, traceback = sys.exc_info()
raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" % raise StudentInputError, ("Could not interpret '%s' as a number" %
cgi.escape(student_answer)), traceback cgi.escape(student_answer)), traceback
if correct: if correct:
...@@ -1072,13 +1078,10 @@ def sympy_check2(): ...@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct'] correct = self.context['correct']
messages = self.context['messages'] messages = self.context['messages']
overall_message = self.context['overall_message'] overall_message = self.context['overall_message']
except Exception as err: except Exception as err:
print "oops in customresponse (code) error %s" % err self._handle_exec_exception(err)
print "context = ", self.context
print traceback.format_exc()
# Notify student
raise StudentInputError(
"Error: Problem could not be evaluated with your input")
else: else:
# self.code is not a string; assume its a function # self.code is not a string; assume its a function
...@@ -1105,13 +1108,9 @@ def sympy_check2(): ...@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs)) nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs) ret = fn(*args[:nargs], **kwargs)
except Exception as err: except Exception as err:
log.error("oops in customresponse (cfn) error %s" % err) self._handle_exec_exception(err)
# print "context = ",self.context
log.error(traceback.format_exc())
raise Exception("oops in customresponse (cfn) error %s" % err)
log.debug(
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret) == dict: if type(ret) == dict:
...@@ -1147,9 +1146,9 @@ def sympy_check2(): ...@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' correct.append('correct'
if input_dict['ok'] else 'incorrect') if input_dict['ok'] else 'incorrect')
msg = (self.clean_message_html(input_dict['msg']) msg = (self.clean_message_html(input_dict['msg'])
if 'msg' in input_dict else None) if 'msg' in input_dict else None)
messages.append(msg) messages.append(msg)
...@@ -1157,7 +1156,7 @@ def sympy_check2(): ...@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception( raise ResponseError(
"CustomResponse: check function returned an invalid dict") "CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
...@@ -1174,7 +1173,7 @@ def sympy_check2(): ...@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]] npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0) if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
...@@ -1227,6 +1226,22 @@ def sympy_check2(): ...@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect} return {self.answer_ids[0]: self.expect}
return self.default_answer_map return self.default_answer_map
def _handle_exec_exception(self, err):
'''
Handle an exception raised during the execution of
custom Python code.
Raises a ResponseError
'''
# Log the error if we are debugging
msg = 'Error occurred while evaluating CustomResponse'
log.warning(msg, exc_info=True)
# Notify student with a student input error
_, _, traceback_obj = sys.exc_info()
raise ResponseError, err.message, traceback_obj
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse): ...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[ submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)] k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
exec self.code in global_context, self.context
try:
exec self.code in global_context, self.context
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ResponseError, ResponseError(err.message), traceback_obj
cmap = CorrectMap() cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct']))) self.answer_ids), self.context['correct'])))
...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], { option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice']) 'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ]) }) for option in self._find_options(inputfield)])
scoring_map[inputfield.get('id')] = option_scoring scoring_map[inputfield.get('id')] = option_scoring
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
% endif % endif
<div id="genex_container"></div> <div id="genex_container"></div>
<input type="hidden" name="dna_sequence" id="dna_sequence" value ="${dna_sequence}"></input> <input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input>
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input> <input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/>
......
...@@ -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)
......
...@@ -13,6 +13,8 @@ import textwrap ...@@ -13,6 +13,8 @@ import textwrap
from . import test_system from . import test_system
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest): ...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest):
# Message is interpreted as an "overall message" # Message is interpreted as an "overall message"
self.assertEqual(correct_map.get_overall_message(), 'Message text') self.assertEqual(correct_map.get_overall_message(), 'Message text')
def test_script_exception(self): def test_script_exception_function(self):
# Construct a script that will raise an exception # Construct a script that will raise an exception
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest): ...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'})
def test_script_exception_inline(self):
# Construct a script that will raise an exception
script = 'raise Exception("Test")'
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self): def test_invalid_dict_exception(self):
...@@ -889,10 +901,70 @@ class CustomResponseTest(ResponseTest): ...@@ -889,10 +901,70 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_module_imports_inline(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
correct[0] = 'correct'
assert('%s' in globals())''' % module_name)
# Create the problem
problem = self.build_problem(answer=script)
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
def test_module_imports_function(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
def check_func(expect, answer_given):
assert('%s' in globals())
return True''' % module_name)
# Create the problem
problem = self.build_problem(script=script, cfn="check_func")
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
class SchematicResponseTest(ResponseTest): class SchematicResponseTest(ResponseTest):
from response_xml_factory import SchematicResponseXMLFactory from response_xml_factory import SchematicResponseXMLFactory
xml_factory_class = SchematicResponseXMLFactory xml_factory_class = SchematicResponseXMLFactory
...@@ -922,6 +994,18 @@ class SchematicResponseTest(ResponseTest): ...@@ -922,6 +994,18 @@ class SchematicResponseTest(ResponseTest):
# is what we expect) # is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
def test_script_exception(self):
# Construct a script that will raise an exception
script = "raise Exception('test')"
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
submission_dict = {'test': 'test'}
input_dict = {'1_2_1': json.dumps(submission_dict)}
problem.grade_answers(input_dict)
class AnnotationResponseTest(ResponseTest): class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory from response_xml_factory import AnnotationResponseXMLFactory
......
...@@ -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
......
...@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')], resource_string(__name__, 'js/src/annotatable/display.coffee')],
'js': [] 'js': []}
}
js_module_name = "Annotatable" js_module_name = "Annotatable"
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable' icon_class = 'annotatable'
...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule):
if color is not None: if color is not None:
if color in self.highlight_colors: if color in self.highlight_colors:
cls.append('highlight-'+color) cls.append('highlight-' + color)
attr['_delete'] = highlight_key attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls) attr['value'] = ' '.join(cls)
return { 'class' : attr } return {'class': attr}
def _get_annotation_data_attr(self, index, el): def _get_annotation_data_attr(self, index, el):
""" Returns a dict in which the keys are the HTML data attributes """ Returns a dict in which the keys are the HTML data attributes
...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
if xml_key in el.attrib: if xml_key in el.attrib:
value = el.get(xml_key, '') value = el.get(xml_key, '')
html_key = attrs_map[xml_key] html_key = attrs_map[xml_key]
data_attrs[html_key] = { 'value': value, '_delete': xml_key } data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs return data_attrs
...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
delete_key = attr[key]['_delete'] delete_key = attr[key]['_delete']
del el.attrib[delete_key] del el.attrib[delete_key]
def _render_content(self): def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """ """ Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content) xmltree = etree.fromstring(self.content)
...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor): ...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
stores_state = True stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
import json
import logging import logging
from lxml import etree from lxml import etree
...@@ -6,14 +5,16 @@ from pkg_resources import resource_string ...@@ -6,14 +5,16 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xblock.core import Integer, Scope, String, Boolean, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod", "max_score"] "skip_spelling_checks", "due", "graceperiod"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
...@@ -49,26 +50,26 @@ class VersionInteger(Integer): ...@@ -49,26 +50,26 @@ 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,
scope=Scope.settings) scope=Scope.settings)
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
scope=Scope.settings) scope=Scope.settings)
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings) scope=Scope.settings)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, 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)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
...@@ -104,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -104,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), js = {'coffee':
resource_string(__name__, 'js/src/collapsible.coffee'), [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
]} resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "CombinedOpenEnded" js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
...@@ -118,7 +120,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -118,7 +120,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -190,8 +192,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -190,8 +192,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def get_score(self): def get_score(self):
return self.child_module.get_score() return self.child_module.get_score()
#def max_score(self): def max_score(self):
# return self.child_module.max_score() return self.child_module.max_score()
def get_progress(self): def get_progress(self):
return self.child_module.get_progress() return self.child_module.get_progress()
...@@ -218,4 +220,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -218,4 +220,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
...@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule ...@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
from xmodule.util.date_utils import time_to_datetime
import json import json
from xblock.core import Scope, List, String, Object, Boolean from xblock.core import Scope, List, String, Object, Boolean
...@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
def _sorting_dates(self): def _sorting_dates(self):
# utility function to get datetime objects for dates used to # utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score # compute the is_new flag and the sorting_score
def to_datetime(timestamp):
return datetime(*timestamp[:6])
announcement = self.announcement announcement = self.announcement
if announcement is not None: if announcement is not None:
announcement = to_datetime(announcement) announcement = time_to_datetime(announcement)
try: try:
start = dateutil.parser.parse(self.advertised_start) start = dateutil.parser.parse(self.advertised_start)
except (ValueError, AttributeError): except (ValueError, AttributeError):
start = to_datetime(self.start) start = time_to_datetime(self.start)
now = to_datetime(time.gmtime()) now = datetime.utcnow()
return announcement, start, now return announcement, start, now
......
/* TODO: move top-level variables to a common _variables.scss.
* NOTE: These variables were only added here because when this was integrated with the CMS,
* SASS compilation errors were triggered because the CMS didn't have the same variables defined
* that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
* -Abarrett and Vshnayder
*/
$border-color: #C8C8C8; $border-color: #C8C8C8;
$body-font-size: em(14); $body-font-size: em(14);
.annotatable-wrapper {
position: relative;
}
.annotatable-header { .annotatable-header {
margin-bottom: .5em; margin-bottom: .5em;
.annotatable-title { .annotatable-title {
...@@ -55,6 +65,7 @@ $body-font-size: em(14); ...@@ -55,6 +65,7 @@ $body-font-size: em(14);
display: inline; display: inline;
cursor: pointer; cursor: pointer;
$highlight_index: 0;
@each $highlight in ( @each $highlight in (
(yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)), (yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)),
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)), (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
...@@ -62,12 +73,13 @@ $body-font-size: em(14); ...@@ -62,12 +73,13 @@ $body-font-size: em(14);
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)), (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)), (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) { (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
$highlight_index: $highlight_index + 1;
$marker: nth($highlight,1); $marker: nth($highlight,1);
$color: nth($highlight,2); $color: nth($highlight,2);
$selected_color: nth($highlight,3); $selected_color: nth($highlight,3);
@if $marker == yellow { @if $highlight_index == 1 {
&.highlight { &.highlight {
background-color: $color; background-color: $color;
&.selected { background-color: $selected_color; } &.selected { background-color: $selected_color; }
...@@ -127,6 +139,7 @@ $body-font-size: em(14); ...@@ -127,6 +139,7 @@ $body-font-size: em(14);
font-weight: 400; font-weight: 400;
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
background-color: transparent; background-color: transparent;
border-color: transparent;
} }
p { p {
color: inherit; color: inherit;
...@@ -143,6 +156,7 @@ $body-font-size: em(14); ...@@ -143,6 +156,7 @@ $body-font-size: em(14);
margin: 0px 0px 10px 0; margin: 0px 0px 10px 0;
max-height: 225px; max-height: 225px;
overflow: auto; overflow: auto;
line-height: normal;
} }
.annotatable-reply { .annotatable-reply {
display: block; display: block;
...@@ -165,5 +179,3 @@ $body-font-size: em(14); ...@@ -165,5 +179,3 @@ $body-font-size: em(14);
border-top-color: rgba(0, 0, 0, .85); border-top-color: rgba(0, 0, 0, .85);
} }
} }
class InvalidDefinitionError(Exception): class InvalidDefinitionError(Exception):
pass pass
class NotFoundError(Exception): class NotFoundError(Exception):
pass pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.
For example: if an exception occurs while checking a capa problem.
'''
pass
import logging import logging
from lxml import etree from lxml import etree
from dateutil import parser
from pkg_resources import resource_string from pkg_resources import resource_string
...@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor ...@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String from xblock.core import Scope, Integer, String
from .fields import Date
from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -16,7 +18,7 @@ class FolditFields(object): ...@@ -16,7 +18,7 @@ class FolditFields(object):
# default to what Spring_7012x uses # default to what Spring_7012x uses
required_level = Integer(default=4, scope=Scope.settings) required_level = Integer(default=4, scope=Scope.settings)
required_sublevel = Integer(default=5, scope=Scope.settings) required_sublevel = Integer(default=5, scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='') due = Date(help="Date that this problem is due by", scope=Scope.settings)
show_basic_score = String(scope=Scope.settings, default='false') show_basic_score = String(scope=Scope.settings, default='false')
show_leaderboard = String(scope=Scope.settings, default='false') show_leaderboard = String(scope=Scope.settings, default='false')
...@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule): ...@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule):
required_sublevel="3" required_sublevel="3"
show_leaderboard="false"/> show_leaderboard="false"/>
""" """
def parse_due_date():
"""
Pull out the date, or None
"""
s = self.due
if s:
return parser.parse(s)
else:
return None
self.due_time = parse_due_date() self.due_time = time_to_datetime(self.due)
def is_complete(self): def is_complete(self):
""" """
...@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor): ...@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
return ({}, []) return {}, []
def definition_to_xml(self): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('foldit') xml_object = etree.Element('foldit')
return xml_object return xml_object
class @Annotatable class @Annotatable
_debug: false _debug: false
# selectors for the annotatable xmodule # selectors for the annotatable xmodule
wrapperSelector: '.annotatable-wrapper'
toggleAnnotationsSelector: '.annotatable-toggle-annotations' toggleAnnotationsSelector: '.annotatable-toggle-annotations'
toggleInstructionsSelector: '.annotatable-toggle-instructions' toggleInstructionsSelector: '.annotatable-toggle-instructions'
instructionsSelector: '.annotatable-instructions' instructionsSelector: '.annotatable-instructions'
...@@ -61,7 +62,7 @@ class @Annotatable ...@@ -61,7 +62,7 @@ class @Annotatable
my: 'bottom center' # of tooltip my: 'bottom center' # of tooltip
at: 'top center' # of target at: 'top center' # of target
target: $(el) # where the tooltip was triggered (i.e. the annotation span) target: $(el) # where the tooltip was triggered (i.e. the annotation span)
container: @$el container: @$(@wrapperSelector)
adjust: adjust:
y: -5 y: -5
show: show:
...@@ -75,6 +76,7 @@ class @Annotatable ...@@ -75,6 +76,7 @@ class @Annotatable
classes: 'ui-tooltip-annotatable' classes: 'ui-tooltip-annotatable'
events: events:
show: @onShowTip show: @onShowTip
move: @onMoveTip
onClickToggleAnnotations: (e) => @toggleAnnotations() onClickToggleAnnotations: (e) => @toggleAnnotations()
...@@ -87,6 +89,55 @@ class @Annotatable ...@@ -87,6 +89,55 @@ class @Annotatable
onShowTip: (event, api) => onShowTip: (event, api) =>
event.preventDefault() if @annotationsHidden event.preventDefault() if @annotationsHidden
onMoveTip: (event, api, position) =>
###
This method handles an edge case in which a tooltip is displayed above
a non-overlapping span like this:
(( TOOLTIP ))
\/
text text text ... text text text ...... <span span span>
<span span span>
The problem is that the tooltip looks disconnected from both spans, so
we should re-position the tooltip to appear above the span.
###
tip = api.elements.tooltip
adjust_y = api.options.position?.adjust?.y || 0
container = api.options.position?.container || $('body')
target = api.elements.target
rects = $(target).get(0).getClientRects()
is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right)
if is_non_overlapping
# we want to choose the largest of the two non-overlapping spans and display
# the tooltip above the center of it (see api.options.position settings)
focus_rect = (if rects[0].width > rects[1].width then rects[0] else rects[1])
rect_center = focus_rect.left + (focus_rect.width / 2)
rect_top = focus_rect.top
tip_width = $(tip).width()
tip_height = $(tip).height()
# tooltip is positioned relative to its container, so we need to factor in offsets
container_offset = $(container).offset()
offset_left = -container_offset.left
offset_top = $(document).scrollTop() - container_offset.top
tip_left = offset_left + rect_center - (tip_width / 2)
tip_top = offset_top + rect_top - tip_height + adjust_y
# make sure the new tip position doesn't clip the edges of the screen
win_width = $(window).width()
if tip_left < offset_left
tip_left = offset_left
else if tip_left + tip_width > win_width + offset_left
tip_left = win_width + offset_left - tip_width
# final step: update the position object (used by qtip2 to show the tip after the move event)
$.extend position, 'left': tip_left, 'top': tip_top
getSpanForProblemReturn: (el) -> getSpanForProblemReturn: (el) ->
problem_id = $(@problemReturnSelector).index(el) problem_id = $(@problemReturnSelector).index(el)
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']") @$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
......
...@@ -356,12 +356,12 @@ def remap_namespace(module, target_location_namespace): ...@@ -356,12 +356,12 @@ def remap_namespace(module, target_location_namespace):
return module return module
def validate_no_non_editable_metadata(module_store, course_id, category, allowed=[]): def validate_no_non_editable_metadata(module_store, course_id, category, allowed=None):
''' '''
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 category 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 + ['xml_attributes', 'display_name'] _allowed = (allowed if allowed is not None else []) + ['xml_attributes', 'display_name']
err_cnt = 0 err_cnt = 0
for module_loc in module_store.modules[course_id]: for module_loc in module_store.modules[course_id]:
...@@ -369,7 +369,7 @@ def validate_no_non_editable_metadata(module_store, course_id, category, allowed ...@@ -369,7 +369,7 @@ def validate_no_non_editable_metadata(module_store, course_id, category, allowed
if module.location.category == category: if module.location.category == category:
my_metadata = dict(own_metadata(module)) my_metadata = dict(own_metadata(module))
for key in my_metadata.keys(): for key in my_metadata.keys():
if key not in allowed: if key not in _allowed:
err_cnt = err_cnt + 1 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]) 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])
......
...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1 MAX_ATTEMPTS = 1
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
#The highest score allowed for the overall xmodule and for each rubric point #The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 50 MAX_SCORE_ALLOWED = 50
...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module(): ...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -143,23 +139,19 @@ class CombinedOpenEndedV1Module(): ...@@ -143,23 +139,19 @@ class CombinedOpenEndedV1Module():
self.accept_file_upload = self.instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.accept_file_upload = self.instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
self.skip_basic_checks = self.instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT self.skip_basic_checks = self.instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT
display_due_date_string = self.instance_state.get('due', None) due_date = self.instance_state.get('due', None)
grace_period_string = self.instance_state.get('graceperiod', None) grace_period_string = self.instance_state.get('graceperiod', None)
try: try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string) self.timeinfo = TimeInfo(due_date, grace_period_string)
except: except:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = self.instance_state.get('max_score', MAX_SCORE)
self.rubric_renderer = CombinedOpenEndedRubric(system, True) self.rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_string = stringify_children(definition['rubric']) rubric_string = stringify_children(definition['rubric'])
self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score) self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
#Static data is passed to the child modules to render #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
......
...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object): ...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return {'success': success, 'html': html, 'rubric_scores': rubric_scores} return {'success': success, 'html': html, 'rubric_scores': rubric_scores}
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
rubric_dict = self.render_rubric(rubric_string) rubric_dict = self.render_rubric(rubric_string)
success = rubric_dict['success'] success = rubric_dict['success']
rubric_feedback = rubric_dict['html'] rubric_feedback = rubric_dict['html']
...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object): ...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if int(total) != int(max_score): return int(total)
#This is a staff_facing_error
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
max_score, location, total)
log.error(error_msg)
raise RubricParsingError(error_msg)
def extract_categories(self, element): def extract_categories(self, element):
''' '''
......
from xblock.core import Integer, Float
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json
"""
def from_json(self, value):
try:
return float(value)
except:
return None
...@@ -6,13 +6,13 @@ from lxml import etree ...@@ -6,13 +6,13 @@ from lxml import etree
from datetime import datetime from datetime import datetime
from pkg_resources import resource_string from pkg_resources import resource_string
from .capa_module import ComplexEncoder from .capa_module import ComplexEncoder
from .stringify import stringify_children
from .x_module import XModule from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -28,13 +28,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please ...@@ -28,13 +28,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.", default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, scope=Scope.settings) link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings) scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
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, scope=Scope.settings) max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state) scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),
scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class PeerGradingModule(PeerGradingFields, XModule): class PeerGradingModule(PeerGradingFields, XModule):
...@@ -72,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -72,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
self._model_data['due'] = due_date self._model_data['due'] = due_date
try: try:
self.timeinfo = TimeInfo(self.display_due_date_string, self.grace_period_string) self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
except: except:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
......
...@@ -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):
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 max_attempts: 1
max_score: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: ""
data: | data: |
<combinedopenended> <combinedopenended>
<rubric> <rubric>
......
...@@ -6,6 +6,7 @@ metadata: ...@@ -6,6 +6,7 @@ metadata:
link_to_location: None link_to_location: None
is_graded: False is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -5,11 +5,15 @@ import unittest ...@@ -5,11 +5,15 @@ import unittest
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from datetime import datetime from datetime import datetime
import logging
log = logging.getLogger(__name__)
from . import test_system from . import test_system
...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self): def test_latest_answer_empty(self):
...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch="score_update"): def constructed_callback(dispatch="score_update"):
return dispatch return dispatch
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback,
'default_queuename': 'testqueue',
'waittime': 1} 'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase): class CombinedOpenEndedModuleTest(unittest.TestCase):
location = Location(["i4x", "edX", "open_ended", "combinedopenended", location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"]) "SampleQuestion"])
definition_template = """
<combinedopenended attempts="10000">
{rubric}
{prompt}
<task>
{task1}
</task>
<task>
{task2}
</task>
</combinedopenended>
"""
prompt = "<prompt>This is a question prompt</prompt>" prompt = "<prompt>This is a question prompt</prompt>"
rubric = '''<rubric><rubric> rubric = '''<rubric><rubric>
<category> <category>
...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
</openendedparam> </openendedparam>
</openended>''' </openended>'''
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
descriptor = Mock() full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
location,
descriptor,
model_data={'data': full_definition, 'weight' : '1'})
def setUp(self): def setUp(self):
self.test_system = test_system()
# TODO: this constructor call is definitely wrong, but neither branch # TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this. # of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system, self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = self.combinedoe.update_task_states() changed = self.combinedoe.update_task_states()
self.assertTrue(changed) self.assertTrue(changed)
def test_get_max_score(self):
changed = self.combinedoe.update_task_states()
self.combinedoe.state = "done"
self.combinedoe.is_scored = True
max_score = self.combinedoe.max_score()
self.assertEqual(max_score, 1)
def test_container_get_max_score(self):
#The progress view requires that this function be exposed
max_score = self.combinedoe_container.max_score()
self.assertEqual(max_score, None)
def test_container_weight(self):
weight = self.combinedoe_container.weight
self.assertEqual(weight,1)
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,6 +42,17 @@ class DummySystem(ImportSystem): ...@@ -39,6 +42,17 @@ 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):
"""Get a dummy course""" """Get a dummy course"""
...@@ -126,10 +140,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -126,10 +140,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)
......
# Tests for xmodule.util.date_utils
from nose.tools import assert_equals
from xmodule.util import date_utils
import datetime
import time
def test_get_time_struct_display():
assert_equals("", date_utils.get_time_struct_display(None, ""))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("03/12/1992", date_utils.get_time_struct_display(test_time, '%m/%d/%Y'))
assert_equals("15:03", date_utils.get_time_struct_display(test_time, '%H:%M'))
def test_get_default_time_display():
assert_equals("", date_utils.get_default_time_display(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("Mar 12, 1992 at 03:03 PM",
date_utils.get_default_time_display(test_time))
def test_time_to_datetime():
assert_equals(None, date_utils.time_to_datetime(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals(datetime.datetime(1992, 3, 12, 15, 3, 30),
date_utils.time_to_datetime(test_time))
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from path import path
import unittest import unittest
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from lxml import etree from lxml import etree
from mock import Mock, patch from mock import Mock, patch
from collections import defaultdict
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import compute_inherited_metadata from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.fields import Date
from .test_export import DATA_DIR from .test_export import DATA_DIR
...@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase):
- inherited metadata doesn't leak to children. - inherited metadata doesn't leak to children.
""" """
system = self.get_system() system = self.get_system()
v = '1 hour' v = 'March 20 17:00'
url_name = 'test1' url_name = 'test1'
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
...@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase):
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
print descriptor, descriptor._model_data print descriptor, descriptor._model_data
self.assertEqual(descriptor.lms.due, v) self.assertEqual(descriptor.lms.due, Date().from_json(v))
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, v) self.assertEqual(child.lms.due, Date().from_json(v))
# Now export and check things # Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
......
import dateutil
import dateutil.parser
import datetime
from .timeparse import parse_timedelta from .timeparse import parse_timedelta
from xmodule.util.date_utils import time_to_datetime
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -9,7 +7,7 @@ log = logging.getLogger(__name__) ...@@ -9,7 +7,7 @@ log = logging.getLogger(__name__)
class TimeInfo(object): class TimeInfo(object):
""" """
This is a simple object that calculates and stores datetime information for an XModule This is a simple object that calculates and stores datetime information for an XModule
based on the due date string and the grace period string based on the due date and the grace period string
So far it parses out three different pieces of time information: So far it parses out three different pieces of time information:
self.display_due_date - the 'official' due date that gets displayed to students self.display_due_date - the 'official' due date that gets displayed to students
...@@ -17,13 +15,10 @@ class TimeInfo(object): ...@@ -17,13 +15,10 @@ class TimeInfo(object):
self.close_date - the real due date self.close_date - the real due date
""" """
def __init__(self, display_due_date_string, grace_period_string): def __init__(self, due_date, grace_period_string):
if display_due_date_string is not None: if due_date is not None:
try: self.display_due_date = time_to_datetime(due_date)
self.display_due_date = dateutil.parser.parse(display_due_date_string)
except ValueError:
log.error("Could not parse due date {0}".format(display_due_date_string))
raise
else: else:
self.display_due_date = None self.display_due_date = None
......
...@@ -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)
......
import time
import datetime
def get_default_time_display(time_struct):
"""
Converts a time struct to a string representation. This is the default
representation used in Studio and LMS.
It is of the form "Apr 09, 2013 at 04:00 PM".
If None is passed in, an empty string will be returned.
"""
return get_time_struct_display(time_struct, "%b %d, %Y at %I:%M %p")
def get_time_struct_display(time_struct, format):
"""
Converts a time struct to a string based on the given format.
If None is passed in, an empty string will be returned.
"""
return '' if time_struct is None else time.strftime(format, time_struct)
def time_to_datetime(time_struct):
"""
Convert a time struct to a datetime.
If None is passed in, None will be returned.
"""
return datetime.datetime(*time_struct[:6]) if time_struct else None
...@@ -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)
...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
else: else:
# VS[compat] # VS[compat]
# cdodge: filesystem static content support. # cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(getattr(self, 'data_dir', None)) caption_asset_path = "/static/subs/"
return self.system.render_template('videoalpha.html', { return self.system.render_template('videoalpha.html', {
'youtube_streams': self.youtube_streams, 'youtube_streams': self.youtube_streams,
......
...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# cdodge: this is a list of metadata names which are 'system' metadata # cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user # and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft'] system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
......
...@@ -12,22 +12,49 @@ ...@@ -12,22 +12,49 @@
} }
} }
//NOTE: // NOTE:
// Genex uses six global functions: // Genex uses 8 global functions, all prefixed with genex:
// genexSetDNASequence (exported from GWT) // 6 are exported from GWT:
// genexSetClickEvent (exported from GWT) // genexSetInitialDNASequence
// genexSetKeyEvent (exported from GWT) // genexSetDNASequence
// genexSetProblemNumber (exported from GWT) // genexGetDNASequence
// genexSetClickEvent
// genexSetKeyEvent
// genexSetProblemNumber
// //
// It calls genexIsReady with a deferred command when it has finished // It calls genexIsReady with a deferred command when it has finished
// initialization and has drawn itself // initialization and has drawn itself
// genexStoreAnswer(answer) is called when the GWT [Store Answer] button // genexStoreAnswer(answer) is called each time the DNA sequence changes
// is clicked // through user interaction
//Genex does not call the following function
genexGetInputField = function() {
var problem = $('#genex_container').parents('.problem');
return problem.find('input[type="hidden"][name!="genex_dna_sequence"][name!="genex_problem_number"]');
};
genexIsReady = function() { genexIsReady = function() {
//Load DNA sequence var input_field = genexGetInputField();
var dna_sequence = $('#dna_sequence').val(); var genex_saved_state = input_field.val();
genexSetDNASequence(dna_sequence); var genex_initial_dna_sequence;
var genex_dna_sequence;
//Get the sequence from xml file
genex_initial_dna_sequence = $('#genex_dna_sequence').val();
//Call this function to set the value used by reset button
genexSetInitialDNASequence(genex_initial_dna_sequence);
if (genex_saved_state === '') {
//Load DNA sequence from xml file
genex_dna_sequence = genex_initial_dna_sequence;
}
else {
//Load DNA sequence from saved value
genex_saved_state = JSON.parse(genex_saved_state);
genex_dna_sequence = genex_saved_state.genex_dna_sequence;
}
genexSetDNASequence(genex_dna_sequence);
//Now load mouse and keyboard handlers //Now load mouse and keyboard handlers
genexSetClickEvent(); genexSetClickEvent();
genexSetKeyEvent(); genexSetKeyEvent();
...@@ -35,10 +62,9 @@ ...@@ -35,10 +62,9 @@
var genex_problem_number = $('#genex_problem_number').val(); var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number); genexSetProblemNumber(genex_problem_number);
}; };
genexStoreAnswer = function(ans) { genexStoreAnswer = function(answer) {
var problem = $('#genex_container').parents('.problem'); var input_field = genexGetInputField();
var input_field = problem.find('input[type="hidden"][name!="dna_sequence"][name!="genex_problem_number"]'); var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer};
input_field.val(ans); input_field.val(JSON.stringify(value));
}; };
}).call(this); }).call(this);
\ No newline at end of file
function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='3F4ADBED36D589545A9300A1EA686D36',Rb='73F4B6D6D466BAD6850A60128DF5B80D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Sb='BA18AC23ACC5016C5D0799E864BBDFFE',ub='Bad handler "',Tb='C7B18436BA03373FB13ED589C2CCF417',cc='DOMContentLoaded',Ub='E1A9A95677AFC620CAD5759B7ACC3E67',Vb='FF175D5583BDD5ACF40C7F0AFF9A374B',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b} function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='46DBCB09BEC38A6DEE76494C6517111B',Rb='557C7018CDCA52B163256408948A1722',Sb='866AF633CAA7EA4DA7E906456CDEC65A',Tb='8F9C3F1A91187AA8391FD08BA7F8716D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Ub='A016796CF7FB22261AE1160531B5CF82',ub='Bad handler "',cc='DOMContentLoaded',Vb='F28D6C3D881F6C18E3357AAB004477EF',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b}
function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}} function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}}
function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P} function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a} function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a}
...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b} ...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b}
function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b} function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null} function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null}
var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}} var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}}
w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Hb],Qb);G([Fb],Rb);G([Ib],Sb);G([Lb],Tb);G([Db],Ub);G([Jb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}} w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Ib],Qb);G([Fb],Rb);G([Db],Sb);G([Hb],Tb);G([Jb],Ub);G([Lb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}}
if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)} if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)}
genex(); genex();
\ No newline at end of file
...@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log ...@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log
rake pylint > pylint.log || cat pylint.log rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0 TESTS_FAILED=0
# Run the python unit tests
rake test_cms[false] || TESTS_FAILED=1 rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
# Don't run the lms jasmine tests for now because
# they mostly all fail anyhow # Run the jaavascript unit tests
# rake phantomjs_jasmine_lms || true rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1 rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1 rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
......
...@@ -73,6 +73,9 @@ class Command(BaseCommand): ...@@ -73,6 +73,9 @@ class Command(BaseCommand):
ended_courses.append(course_id) ended_courses.append(course_id)
for course_id in ended_courses: for course_id in ended_courses:
# prefetch all chapters/sequentials by saying depth=2
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
print "Fetching enrolled students for {0}".format(course_id) print "Fetching enrolled students for {0}".format(course_id)
enrolled_students = User.objects.filter( enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related( courseenrollment__course_id=course_id).prefetch_related(
...@@ -99,6 +102,6 @@ class Command(BaseCommand): ...@@ -99,6 +102,6 @@ class Command(BaseCommand):
student, course_id)['status'] in valid_statuses: student, course_id)['status'] in valid_statuses:
if not options['noop']: if not options['noop']:
# Add the certificate request to the queue # Add the certificate request to the queue
ret = xq.add_cert(student, course_id) ret = xq.add_cert(student, course_id, course=course)
if ret == 'generating': if ret == 'generating':
print '{0} - {1}'.format(student, ret) print '{0} - {1}'.format(student, ret)
...@@ -115,7 +115,7 @@ class XQueueCertInterface(object): ...@@ -115,7 +115,7 @@ class XQueueCertInterface(object):
raise NotImplementedError raise NotImplementedError
def add_cert(self, student, course_id): def add_cert(self, student, course_id, course=None):
""" """
Arguments: Arguments:
...@@ -151,9 +151,12 @@ class XQueueCertInterface(object): ...@@ -151,9 +151,12 @@ class XQueueCertInterface(object):
if cert_status in VALID_STATUSES: if cert_status in VALID_STATUSES:
# grade the student # grade the student
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
# re-use the course passed in optionally so we don't have to re-fetch everything
# for every student
if course is None:
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id) user=student, course_id=course_id)
......
...@@ -13,7 +13,6 @@ from xblock.core import Scope ...@@ -13,7 +13,6 @@ from xblock.core import Scope
from .module_render import get_module, get_module_for_descriptor from .module_render import get_module, get_module_for_descriptor
from xmodule import graders from xmodule import graders
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score from xmodule.graders import Score
from .models import StudentModule from .models import StudentModule
...@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator): ...@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
else: else:
return descriptor.get_children() return descriptor.get_children()
stack = [descriptor] stack = [descriptor]
while len(stack) > 0: while len(stack) > 0:
...@@ -66,7 +64,7 @@ def yield_problems(request, course, student): ...@@ -66,7 +64,7 @@ def yield_problems(request, course, student):
).values_list('module_state_key', flat=True)) ).values_list('module_state_key', flat=True))
sections_to_list = [] sections_to_list = []
for section_format, sections in grading_context['graded_sections'].iteritems(): for _, sections in grading_context['graded_sections'].iteritems():
for section in sections: for section in sections:
section_descriptor = section['section_descriptor'] section_descriptor = section['section_descriptor']
...@@ -123,7 +121,7 @@ def answer_distributions(request, course): ...@@ -123,7 +121,7 @@ def answer_distributions(request, course):
def grade(student, request, course, model_data_cache=None, keep_raw_scores=False): def grade(student, request, course, model_data_cache=None, keep_raw_scores=False):
""" """
This grades a student as quickly as possible. It retuns the This grades a student as quickly as possible. It returns the
output from the course grader, augmented with the final letter output from the course grader, augmented with the final letter
grade. The keys in the output are: grade. The keys in the output are:
...@@ -158,10 +156,16 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -158,10 +156,16 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
should_grade_section = False should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0% # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for moduledescriptor in section['xmoduledescriptors']: for moduledescriptor in section['xmoduledescriptors']:
# some problems have state that is updated independently of interaction
# with the LMS, so they need to always be scored. (E.g. foldit.)
if moduledescriptor.always_recalculate_grades:
should_grade_section = True
break
# 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
...@@ -174,7 +178,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -174,7 +178,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
scores = [] scores = []
def create_module(descriptor): def create_module(descriptor):
# TODO: We need the request to pass into here. If we could forgo that, our arguments '''creates an XModule instance given a descriptor'''
# TODO: We need the request to pass into here. If we could forego that, our arguments
# would be simpler # would be simpler
return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id) return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id)
...@@ -197,18 +202,18 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -197,18 +202,18 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default)) scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
section_total, graded_total = graders.aggregate_scores(scores, section_name) _, graded_total = graders.aggregate_scores(scores, section_name)
if keep_raw_scores: if keep_raw_scores:
raw_scores += scores raw_scores += scores
else: else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name) graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores #Add the graded total to totaled_scores
if graded_total.possible > 0: if graded_total.possible > 0:
format_scores.append(graded_total) format_scores.append(graded_total)
else: else:
log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.location)) log.exception("Unable to grade a section with a total possible score of zero. " +
str(section_descriptor.location))
totaled_scores[section_format] = format_scores totaled_scores[section_format] = format_scores
...@@ -274,12 +279,9 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -274,12 +279,9 @@ def progress_summary(student, request, course, model_data_cache):
""" """
# TODO: We need the request to pass into here. If we could forego that, our arguments
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler # would be simpler
course_module = get_module(student, request, course_module = get_module(student, request, course.location, model_data_cache, course.id, depth=None)
course.location, model_data_cache,
course.id, depth=None)
if not course_module: if not course_module:
# This student must not have access to the course. # This student must not have access to the course.
return None return None
...@@ -310,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -310,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache):
if correct is None and total is None: if correct is None and total is None:
continue continue
scores.append(Score(correct, total, graded, scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
module_descriptor.display_name_with_default))
scores.reverse() scores.reverse()
section_total, graded_total = graders.aggregate_scores( section_total, _ = graders.aggregate_scores(
scores, section_module.display_name_with_default) scores, section_module.display_name_with_default)
format = section_module.lms.format if section_module.lms.format is not None else '' module_format = section_module.lms.format if section_module.lms.format is not None else ''
sections.append({ sections.append({
'display_name': section_module.display_name_with_default, 'display_name': section_module.display_name_with_default,
'url_name': section_module.url_name, 'url_name': section_module.url_name,
'scores': scores, 'scores': scores,
'section_total': section_total, 'section_total': section_total,
'format': format, 'format': module_format,
'due': section_module.lms.due, 'due': section_module.lms.due,
'graded': graded, 'graded': graded,
}) })
...@@ -353,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -353,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if not user.is_authenticated(): if not user.is_authenticated():
return (None, None) return (None, None)
# some problems have state that is updated independently of interaction
# with the LMS, so they need to always be scored. (E.g. foldit.)
if problem_descriptor.always_recalculate_grades: if problem_descriptor.always_recalculate_grades:
problem = module_creator(problem_descriptor) problem = module_creator(problem_descriptor)
d = problem.get_score() score = problem.get_score()
if d is not None: if score is not None:
return (d['score'], d['total']) return (score['score'], score['total'])
else: else:
return (None, None) return (None, None)
...@@ -367,7 +370,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -367,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
...@@ -394,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -394,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if total is None: if total is None:
return (None, None) return (None, None)
#Now we re-weight the problem, if specified # Now we re-weight the problem, if specified
weight = problem_descriptor.weight weight = problem_descriptor.weight
if weight is not None: if weight is not None:
if total == 0: if total == 0:
......
'''
This is a one-off command aimed at fixing a temporary problem encountered where input_state was added to
the same dict object in capa problems, so was accumulating. The fix is simply to remove input_state entry
from state for all problems in the affected date range.
'''
import json
import logging
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from courseware.models import StudentModule, StudentModuleHistory
LOG = logging.getLogger(__name__)
class Command(BaseCommand):
'''
The fix here is to remove the "input_state" entry in the StudentModule objects of any problems that
contain them. No problem is yet making use of this, and the code should do the right thing if it's
missing (by recreating an empty dict for its value).
To narrow down the set of problems that might need fixing, the StudentModule
objects to be checked is filtered down to those:
created < '2013-03-29 16:30:00' (the problem must have been answered before the buggy code was reverted,
on Prod and Edge)
modified > '2013-03-28 22:00:00' (the problem must have been visited after the bug was introduced
on Prod and Edge)
state like '%input_state%' (the problem must have "input_state" set).
This filtering is done on the production database replica, so that the larger select queries don't lock
the real production database. The list of id values for Student Modules is written to a file, and the
file is passed into this command. The sql file passed to mysql contains:
select sm.id from courseware_studentmodule sm
where sm.modified > "2013-03-28 22:00:00"
and sm.created < "2013-03-29 16:30:00"
and sm.state like "%input_state%"
and sm.module_type = 'problem';
'''
num_visited = 0
num_changed = 0
num_hist_visited = 0
num_hist_changed = 0
option_list = BaseCommand.option_list + (
make_option('--save',
action='store_true',
dest='save_changes',
default=False,
help='Persist the changes that were encountered. If not set, no changes are saved.'),
)
def fix_studentmodules_in_list(self, save_changes, idlist_path):
'''Read in the list of StudentModule objects that might need fixing, and then fix each one'''
# open file and read id values from it:
for line in open(idlist_path, 'r'):
student_module_id = line.strip()
# skip the header, if present:
if student_module_id == 'id':
continue
try:
module = StudentModule.objects.get(id=student_module_id)
except StudentModule.DoesNotExist:
LOG.error("Unable to find student module with id = {0}: skipping... ".format(student_module_id))
continue
self.remove_studentmodule_input_state(module, save_changes)
hist_modules = StudentModuleHistory.objects.filter(student_module_id=student_module_id)
for hist_module in hist_modules:
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
def remove_studentmodule_input_state(self, module, save_changes):
''' Fix the grade assigned to a StudentModule'''
module_state = module.state
if module_state is None:
# not likely, since we filter on it. But in general...
LOG.info("No state found for {type} module {id} for student {student} in course {course_id}"
.format(type=module.module_type, id=module.module_state_key,
student=module.student.username, course_id=module.course_id))
return
state_dict = json.loads(module_state)
self.num_visited += 1
if 'input_state' not in state_dict:
pass
elif save_changes:
# make the change and persist
del state_dict['input_state']
module.state = json.dumps(state_dict)
module.save()
self.num_changed += 1
else:
# don't make the change, but increment the count indicating the change would be made
self.num_changed += 1
@transaction.autocommit
def remove_studentmodulehistory_input_state(self, module, save_changes):
''' Fix the grade assigned to a StudentModule'''
module_state = module.state
if module_state is None:
# not likely, since we filter on it. But in general...
LOG.info("No state found for {type} module {id} for student {student} in course {course_id}"
.format(type=module.module_type, id=module.module_state_key,
student=module.student.username, course_id=module.course_id))
return
state_dict = json.loads(module_state)
self.num_hist_visited += 1
if 'input_state' not in state_dict:
pass
elif save_changes:
# make the change and persist
del state_dict['input_state']
module.state = json.dumps(state_dict)
module.save()
self.num_hist_changed += 1
else:
# don't make the change, but increment the count indicating the change would be made
self.num_hist_changed += 1
def handle(self, *args, **options):
'''Handle management command request'''
if len(args) != 1:
raise CommandError("missing idlist file")
idlist_path = args[0]
save_changes = options['save_changes']
LOG.info("Starting run: reading from idlist file {0}; save_changes = {1}".format(idlist_path, save_changes))
self.fix_studentmodules_in_list(save_changes, idlist_path)
LOG.info("Finished run: updating {0} of {1} student modules".format(self.num_changed, self.num_visited))
LOG.info("Finished run: updating {0} of {1} student history modules".format(self.num_hist_changed,
self.num_hist_visited))
...@@ -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,7 +237,7 @@ class ModelDataCache(object): ...@@ -237,7 +237,7 @@ 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,
...@@ -255,13 +255,13 @@ class ModelDataCache(object): ...@@ -255,13 +255,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 +281,12 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -281,12 +281,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 +297,9 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -297,9 +297,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 +321,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -321,7 +321,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 +335,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -335,7 +335,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 +355,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -355,7 +355,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 +377,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -377,7 +377,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:
......
...@@ -11,7 +11,7 @@ from django.contrib.auth.models import User ...@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
...@@ -23,7 +23,7 @@ from .models import StudentModule ...@@ -23,7 +23,7 @@ from .models import StudentModule
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, p) ajax_return = instance.handle_ajax(dispatch, p)
# If we can't find the module, respond with a 404
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
# For XModule-specific errors, we respond with 400
except ProcessingError:
log.warning("Module encountered an error while prcessing AJAX call",
exc_info=True)
return HttpResponseBadRequest()
# If any other error occurred, re-raise it to trigger a 500 response
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
......
...@@ -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
...@@ -124,17 +124,17 @@ class TestTOC(TestCase): ...@@ -124,17 +124,17 @@ class TestTOC(TestCase):
expected = ([{'active': True, 'sections': expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False}, 'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': None, 'active': False},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
...@@ -151,17 +151,17 @@ class TestTOC(TestCase): ...@@ -151,17 +151,17 @@ class TestTOC(TestCase):
expected = ([{'active': True, 'sections': expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': '', 'active': False}, 'format': u'Lecture Sequence', 'due': None, 'active': False},
{'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True, {'url_name': 'Welcome', 'display_name': u'Welcome', 'graded': True,
'format': '', 'due': '', 'active': True}, 'format': '', 'due': None, 'active': True},
{'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True, {'url_name': 'video_123456789012', 'display_name': 'video 123456789012', 'graded': True,
'format': '', 'due': '', 'active': False}, 'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'video 4f66f493ac8f', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': '', 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache)
......
...@@ -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()
...@@ -663,13 +664,13 @@ def submission_history(request, course_id, student_username, location): ...@@ -663,13 +664,13 @@ def submission_history(request, course_id, student_username, location):
.format(student_username, location)) .format(student_username, location))
history_entries = StudentModuleHistory.objects \ history_entries = StudentModuleHistory.objects \
.filter(student_module=student_module).order_by('-created') .filter(student_module=student_module).order_by('-id')
# If no history records exist, let's force a save to get history started. # If no history records exist, let's force a save to get history started.
if not history_entries: if not history_entries:
student_module.save() student_module.save()
history_entries = StudentModuleHistory.objects \ history_entries = StudentModuleHistory.objects \
.filter(student_module=student_module).order_by('-created') .filter(student_module=student_module).order_by('-id')
context = { context = {
'history_entries': history_entries, 'history_entries': history_entries,
......
...@@ -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
...@@ -74,8 +74,8 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): ...@@ -74,8 +74,8 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
# All the not-actually-in-the-course hw and labs come from the # All the not-actually-in-the-course hw and labs come from the
# default grading policy string in graders.py # default grading policy string in graders.py
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm 01","Midterm Avg","Final 01","Final Avg" expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
''' '''
self.assertEqual(body, expected_body, msg) self.assertEqual(body, expected_body, msg)
......
...@@ -229,9 +229,11 @@ def instructor_dashboard(request, course_id): ...@@ -229,9 +229,11 @@ def instructor_dashboard(request, course_id):
if student_to_reset is not None: if student_to_reset is not None:
# find the module in question # find the module in question
if '/' not in problem_to_reset: # allow state of modules other than problem to be reset
problem_to_reset = "problem/" + problem_to_reset # but problem is the default
try: try:
(org, course_name, run) = course_id.split("/") (org, course_name, run) = course_id.split("/")
module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_reset module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id, module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
course_id=course_id, course_id=course_id,
module_state_key=module_state_key) module_state_key=module_state_key)
......
...@@ -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,13 +245,16 @@ def generate_plots_for_problem(problem): ...@@ -246,13 +245,16 @@ 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
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0]) try:
yset['fitparam'] = cfp cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0]) yset['fitparam'] = cfp
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])] yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
fitx = np.linspace(xdat[0], xdat[-1], 100) yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
yset['fitx'] = fitx fitx = np.linspace(xdat[0], xdat[-1], 100)
yset['fity'] = func_2pl(np.array(fitx), *cfp[0]) yset['fitx'] = fitx
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
...@@ -289,7 +291,7 @@ def generate_plots_for_problem(problem): ...@@ -289,7 +291,7 @@ def generate_plots_for_problem(problem):
'info': '', 'info': '',
'data': jsdata, 'data': jsdata,
'cmd': '[%s], %s' % (','.join(jsplots), axisopts), 'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
}) })
#log.debug('plots = %s' % plots) #log.debug('plots = %s' % plots)
return msg, plots return msg, plots
...@@ -302,12 +304,12 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key): ...@@ -302,12 +304,12 @@ 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,
defaults={'state': '{}', 'module_type': 'problem'}, defaults={'state': '{}', 'module_type': 'problem'},
) )
try: try:
pmd = PsychometricData.objects.using(db).get(studentmodule=sm) pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
...@@ -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:
......
...@@ -221,7 +221,7 @@ FILE_UPLOAD_HANDLERS = ( ...@@ -221,7 +221,7 @@ FILE_UPLOAD_HANDLERS = (
########################### PIPELINE ################################# ########################### PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
########################## PEARSON TESTING ########################### ########################## PEARSON TESTING ###########################
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
......
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%! from xmodule.util.date_utils import get_default_time_display %>
<%def name="make_chapter(chapter)"> <%def name="make_chapter(chapter)">
<div class="chapter"> <div class="chapter">
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}"> <li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}"> <a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
<p>${section['display_name']}</p> <p>${section['display_name']}</p>
<p class="subtitle">${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}</p> <p class="subtitle">${section['format']} ${"due " + get_default_time_display(section['due']) if section.get('due') is not None else ''}</p>
</a> </a>
</li> </li>
% endfor % endfor
......
...@@ -190,6 +190,7 @@ function goto( mode) ...@@ -190,6 +190,7 @@ function goto( mode)
<input type="submit" name="action" value="Export CSV file of grades for assignment"> <input type="submit" name="action" value="Export CSV file of grades for assignment">
</li> </li>
</ul> </ul>
<hr width="40%" style="align:left">
%endif %endif
...@@ -197,11 +198,13 @@ function goto( mode) ...@@ -197,11 +198,13 @@ function goto( mode)
<p>edX email address or their username: </p> <p>edX email address or their username: </p>
<p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p> <p><input type="text" name="unique_student_identifier"> <input type="submit" name="action" value="Get link to student's progress page"></p>
<p>and, if you want to reset the number of attempts for a problem, the urlname of that problem</p> <p>and, if you want to reset the number of attempts for a problem, the urlname of that problem</p>
<p> <input type="text" name="problem_to_reset"> <input type="submit" name="action" value="Reset student's attempts"> </p> <p> <input type="text" name="problem_to_reset" size="60"> <input type="submit" name="action" value="Reset student's attempts"> </p>
%if instructor_access: %if instructor_access:
<p> You may also delete the entire state of a student for a problem: <p> You may also delete the entire state of a student for a problem:
<input type="submit" name="action" value="Delete student state for problem"> </p> <input type="submit" name="action" value="Delete student state for problem"> </p>
<p>To delete the state of other XBlocks specify modulename/urlname, eg
<tt>combinedopenended/Humanities_SA_Peer</tt></p>
%endif %endif
%endif %endif
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
%> %>
<%! from xmodule.util.date_utils import get_default_time_display %>
<%block name="js_extra"> <%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
...@@ -29,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", ...@@ -29,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:
...@@ -60,9 +62,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", ...@@ -60,9 +62,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<p> <p>
${section['format']} ${section['format']}
%if 'due' in section and section['due']!="": %if section.get('due') is not None:
<em> <em>
due ${section['due']} due ${get_default_time_display(section['due'])}
</em> </em>
%endif %endif
</p> </p>
......
<%!
from xmodule.util.date_utils import get_default_time_display
%>
<div class="folditbasic"> <div class="folditbasic">
<p><strong>Due:</strong> ${due} <p><strong>Due:</strong> ${get_default_time_display(due)}
<p> <p>
<strong>Status:</strong> <strong>Status:</strong>
......
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