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=
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
disable=
# C0301: Line too long
# W0141: Used builtin function 'map'
# W0142: Used * or ** magic
# R0201: Method could be a function
......@@ -42,7 +43,7 @@ disable=
# R0903: Too few public methods (1/2)
# R0904: Too many public methods
# R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS]
......
......@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
import time
from logging import getLogger
logger = getLogger(__name__)
......@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'):
save_css = 'input.new-subsection-name-save'
world.css_fill(name_css, name)
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 @@
from lettuce import world, step
from common import *
from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS ####################
......@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step):
@step('I save a new section release date$')
def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker'
time_css = 'input.start-time.time.ui-timepicker-input'
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))
set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
'input.start-time.time.ui-timepicker-input', '12:00am')
world.browser.click_link_by_text('Save')
......
......@@ -25,6 +25,13 @@ Feature: Create Subsection
And I reload the page
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
Scenario: Delete a subsection
Given I have opened a new course section in Studio
......@@ -33,3 +40,5 @@ Feature: Create Subsection
When I press the "subsection" delete icon
And I confirm the alert
Then the subsection does not exist
......@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step):
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')
def i_click_the_new_subsection_link(step):
world.css_click('a.new-subsection-item')
......@@ -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"')
@step('I have added a new subsection$')
def i_have_added_a_new_subsection(step):
add_subsection()
@step('I have set a release date and due date in different years$')
def test_have_set_dates_in_different_years(step):
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$')
......
......@@ -151,10 +151,6 @@ def compute_unit_state(unit):
return UnitState.public
def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value):
"""
If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
......
......@@ -6,7 +6,6 @@ import sys
import time
import tarfile
import shutil
from datetime import datetime
from collections import defaultdict
from uuid import uuid4
from path import path
......@@ -42,17 +41,18 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
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 xmodule.contentstore.django import contentstore
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 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 .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
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -365,7 +365,7 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'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,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
......@@ -439,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
......@@ -821,7 +828,7 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location)
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),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
......@@ -1426,7 +1433,7 @@ def asset_index(request, org, course, name):
id = asset['_id']
display_info = {}
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'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
......
......@@ -35,6 +35,7 @@ MITX_FEATURES = {
'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
}
ENABLE_JASMINE = False
......
......@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev
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 #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
......@@ -143,3 +147,6 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
......@@ -4,6 +4,9 @@ var $modalCover;
var $newComponentItem;
var $changedInput;
var $spinner;
var $newComponentTypePicker;
var $newComponentTemplatePickers;
var $newComponentButton;
$(document).ready(function () {
$body = $('body');
......@@ -83,6 +86,8 @@ $(document).ready(function () {
// general link management - smooth scrolling page links
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
// toggling overview section details
$(function () {
......@@ -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) {
window.open($(e.target).attr('href'));
e.preventDefault();
......@@ -228,7 +245,7 @@ function syncReleaseDate(e) {
$("#start_time").val("");
}
function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
function getEdxTimeFromDateTimeVals(date_val, time_val) {
var edxTimeStr = null;
if (date_val != '') {
......@@ -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
var date = Date.parse(date_val + " " + time_val);
if (format == null)
format = 'yyyy-MM-ddTHH:mm';
edxTimeStr = date.toString(format);
edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
}
return edxTimeStr;
}
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) {
function getEdxTimeFromDateTimeInputs(date_id, time_id) {
var input_date = $('#' + date_id).val();
var input_time = $('#' + time_id).val();
return getEdxTimeFromDateTimeVals(input_date, input_time, format);
return getEdxTimeFromDateTimeVals(input_date, input_time);
}
function autosaveInput(e) {
......@@ -291,10 +305,8 @@ function saveSubsection() {
}
// 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['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm');
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time');
$.ajax({
url: "/save_item",
......@@ -316,8 +328,8 @@ function saveSubsection() {
function createNewUnit(e) {
e.preventDefault();
parent = $(this).data('parent');
template = $(this).data('template');
var parent = $(this).data('parent');
var template = $(this).data('template');
$.post('/clone_item',
{'parent_location': parent,
......
......@@ -644,7 +644,7 @@ hr.divide {
position: absolute;
top: 0;
left: 0;
z-index: 99999;
z-index: 10000;
padding: 0 10px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.85);
......
......@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125);
$black-t1: rgba(0,0,0,0.25);
$black-t2: rgba(0,0,0,0.50);
$black-t3: rgba(0,0,0,0.75);
$white: rgb(255,255,255);
$white-t0: rgba(255,255,255,0.125);
$white-t1: rgba(255,255,255,0.25);
......
......@@ -27,7 +27,8 @@
@import 'elements/forms';
@import 'elements/modal';
@import 'elements/alerts';
@import 'elements/jquery-ui-calendar';
@import 'elements/vendor';
@import 'elements/tender-widget';
// specific views
@import 'views/account';
......
......@@ -132,7 +132,7 @@
// specific elements - course nav
.nav-course {
width: 335px;
width: 285px;
margin-top: -($baseline/4);
@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 {
border-color: $darkGrey;
border-radius: 2px;
......@@ -8,6 +9,7 @@
font-family: $sans-serif;
font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important;
.ui-widget-header {
background: $darkGrey;
......@@ -53,4 +55,11 @@
border-color: $orange;
color: #fff;
}
}
// ====================
// JQUI timepicker
.ui-timepicker-list {
z-index: 100000 !important;
}
\ No newline at end of file
......@@ -26,7 +26,7 @@ body.course.outline {
position: relative;
top: -4px;
right: 50px;
width: 145px;
width: 100px;
.status-label {
position: absolute;
......@@ -62,7 +62,7 @@ body.course.outline {
opacity: 0.0;
position: absolute;
top: -1px;
left: 5px;
right: 0;
margin: 0;
padding: 8px 12px;
background: $white;
......@@ -160,7 +160,7 @@ body.course.outline {
.section-published-date {
position: absolute;
top: 19px;
right: 90px;
right: 80px;
padding: 4px 10px;
border-radius: 3px;
background: $lightGrey;
......@@ -271,8 +271,6 @@ body.course.outline {
.section-published-date {
float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px);
background: $lightGrey;
......
......@@ -55,9 +55,11 @@
<%block name="content"></%block>
<%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
<%block name="jsextra"></%block>
</body>
<%include file="widgets/qualaroo.html" />
</html>
<%inherit file="base.html" />
<%!
from time import mktime
import dateutil.parser
import logging
from datetime import datetime
from xmodule.util.date_utils import get_time_struct_display
%>
<%! from django.core.urlresolvers import reverse %>
......@@ -13,7 +11,6 @@
<%namespace name="units" file="widgets/units.html" />
<%namespace name='static' file='static_content.html'/>
<%namespace name='datetime' module='datetime'/>
<%block name="content">
<div class="main-wrapper">
......@@ -38,18 +35,15 @@
<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>
<div class="datepair" data-language="javascript">
<%
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
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"/>
<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"/>
<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"/>
</div>
% 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.
% 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
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif
......@@ -66,12 +60,8 @@
<a href="#" class="set-date">Set a due date</a>
<div class="datepair date-setter">
<p class="date-description">
<%
# 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
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"/>
<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"/>
<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"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
</div>
......
......@@ -69,7 +69,7 @@
<article class="my-classes">
% if user.is_active:
<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>
<a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span>
......
<%inherit file="base.html" />
<%!
from time import mktime
import dateutil.parser
import logging
from datetime import datetime
from xmodule.util.date_utils import get_time_struct_display
%>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Outline</%block>
......@@ -163,11 +161,10 @@
</h3>
<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 = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
start_time_str = get_time_struct_display(section.lms.start, '%I:%M %p')
%>
%if start_date is None:
%if section.lms.start is None:
<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>
%else:
......
......@@ -14,15 +14,14 @@
<li class="nav-item nav-peripheral-pp">
<a href="#">Privacy Policy</a>
</li> -->
<li class="nav-item nav-peripheral-help">
<a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a>
</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():
<!-- 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
</ol>
</nav>
......
<%! from django.core.urlresolvers import reverse %>
<div class="wrapper-header wrapper">
<div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner">
<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=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 .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
from nose.tools import assert_equals, assert_in
import time
from logging import getLogger
logger = getLogger(__name__)
......@@ -125,11 +134,6 @@ def i_am_logged_in(step):
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$')
def i_am_an_edx_user(step):
world.create_user('robot')
......
......@@ -32,6 +32,8 @@ from copy import deepcopy
import chem
import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers
import verifiers.draganddrop
......@@ -67,6 +69,9 @@ global_context = {'random': random,
'scipy': scipy,
'calc': calc,
'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements
......
......@@ -655,9 +655,9 @@ class MatlabInput(CodeInput):
# Check if problem has been queued
self.queuename = 'matlab'
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']
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.queue_len = 1
self.msg = self.plot_submitted_msg
......@@ -702,7 +702,7 @@ class MatlabInput(CodeInput):
def _extra_context(self):
''' Set up additional context variables'''
extra_context = {
'queue_len': self.queue_len,
'queue_len': str(self.queue_len),
'queue_msg': self.queue_msg
}
return extra_context
......@@ -1140,12 +1140,13 @@ registry.register(DesignProtein2dInput)
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:
<editagene width="800" hight="500" dna_sequence="ETAAGGCTATAACCGA" />
"""
<editagene genex_dna_sequence="CGAT" genex_problem_number="1"/>
"""
template = "editageneinput.html"
tags = ['editageneinput']
......@@ -1155,9 +1156,7 @@ class EditAGeneInput(InputTypeBase):
"""
Note: width, height, and dna_sequencee are required.
"""
return [Attribute('width'),
Attribute('height'),
Attribute('dna_sequence'),
return [Attribute('genex_dna_sequence'),
Attribute('genex_problem_number')
]
......
......@@ -17,6 +17,7 @@ import logging
import numbers
import numpy
import os
import sys
import random
import re
import requests
......@@ -52,12 +53,17 @@ class LoncapaProblemError(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
class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass
#-----------------------------------------------------------------------------
......@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys
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
if correct:
......@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct']
messages = self.context['messages']
overall_message = self.context['overall_message']
except Exception as err:
print "oops in customresponse (code) error %s" % err
print "context = ", self.context
print traceback.format_exc()
# Notify student
raise StudentInputError(
"Error: Problem could not be evaluated with your input")
self._handle_exec_exception(err)
else:
# self.code is not a string; assume its a function
......@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs)
except Exception as err:
log.error("oops in customresponse (cfn) error %s" % 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)
self._handle_exec_exception(err)
if type(ret) == dict:
......@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = []
messages = []
for input_dict in input_list:
correct.append('correct'
correct.append('correct'
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)
messages.append(msg)
......@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception
else:
log.error(traceback.format_exc())
raise Exception(
raise ResponseError(
"CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value,
......@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]]
npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints)
......@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect}
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):
submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)]
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.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct'])))
......@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], {
'correctness': choices.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
......
......@@ -13,7 +13,7 @@
% endif
<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="input_${id}" id="input_${id}" value="${value|h}"/>
......
......@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
input_class = lookup_tag('matlabinput')
the_input = self.input_class(test_system, elt, state)
context = the_input._get_render_context()
......@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase):
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):
get = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", get)
......@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase):
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
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
from . import test_system
import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat
......@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest):
# Message is interpreted as an "overall message"
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
script = textwrap.dedent("""
......@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func")
# 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'})
def test_invalid_dict_exception(self):
......@@ -889,10 +901,70 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func")
# 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_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):
from response_xml_factory import SchematicResponseXMLFactory
xml_factory_class = SchematicResponseXMLFactory
......@@ -922,6 +994,18 @@ class SchematicResponseTest(ResponseTest):
# is what we expect)
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):
from response_xml_factory import AnnotationResponseXMLFactory
......
......@@ -33,7 +33,7 @@ def group_from_value(groups, v):
class ABTestFields(object):
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: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True
......
......@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')],
'js': []
}
'js': []}
js_module_name = "Annotatable"
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable'
......@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule):
if color is not None:
if color in self.highlight_colors:
cls.append('highlight-'+color)
cls.append('highlight-' + color)
attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls)
return { 'class' : attr }
return {'class': attr}
def _get_annotation_data_attr(self, index, el):
""" Returns a dict in which the keys are the HTML data attributes
......@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
if xml_key in el.attrib:
value = el.get(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
......@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
delete_key = attr[key]['_delete']
del el.attrib[delete_key]
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
......@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
stores_state = True
template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html"
import json
import logging
from lxml import etree
......@@ -6,14 +5,16 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
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 collections import namedtuple
from .fields import Date
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware")
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",
"student_attempts", "ready_to_reset"]
......@@ -49,26 +50,26 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object):
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)
task_states = List(help="List of state dictionaries of each task within this module.", 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.user_state)
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,
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,
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)
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,
scope=Scope.settings)
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
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,
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)
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):
......@@ -104,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js = {'coffee':
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
......@@ -118,7 +120,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file:
<combinedopenended attempts="10000" max_score="1">
<combinedopenended attempts="10000">
<rubric>
Blah blah rubric.
</rubric>
......@@ -190,8 +192,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def get_score(self):
return self.child_module.get_score()
#def max_score(self):
# return self.child_module.max_score()
def max_score(self):
return self.child_module.max_score()
def get_progress(self):
return self.child_module.get_progress()
......@@ -218,4 +220,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True
has_score = True
template_dir_name = "combinedopenended"
......@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
from xmodule.util.date_utils import time_to_datetime
import json
from xblock.core import Scope, List, String, Object, Boolean
......@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
def _sorting_dates(self):
# utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score
def to_datetime(timestamp):
return datetime(*timestamp[:6])
announcement = self.announcement
if announcement is not None:
announcement = to_datetime(announcement)
announcement = time_to_datetime(announcement)
try:
start = dateutil.parser.parse(self.advertised_start)
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
......
/* 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;
$body-font-size: em(14);
.annotatable-wrapper {
position: relative;
}
.annotatable-header {
margin-bottom: .5em;
.annotatable-title {
......@@ -55,6 +65,7 @@ $body-font-size: em(14);
display: inline;
cursor: pointer;
$highlight_index: 0;
@each $highlight in (
(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)),
......@@ -62,12 +73,13 @@ $body-font-size: em(14);
(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)),
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
$highlight_index: $highlight_index + 1;
$marker: nth($highlight,1);
$color: nth($highlight,2);
$selected_color: nth($highlight,3);
@if $marker == yellow {
@if $highlight_index == 1 {
&.highlight {
background-color: $color;
&.selected { background-color: $selected_color; }
......@@ -127,6 +139,7 @@ $body-font-size: em(14);
font-weight: 400;
padding: 0 10px 10px 10px;
background-color: transparent;
border-color: transparent;
}
p {
color: inherit;
......@@ -143,6 +156,7 @@ $body-font-size: em(14);
margin: 0px 0px 10px 0;
max-height: 225px;
overflow: auto;
line-height: normal;
}
.annotatable-reply {
display: block;
......@@ -165,5 +179,3 @@ $body-font-size: em(14);
border-top-color: rgba(0, 0, 0, .85);
}
}
class InvalidDefinitionError(Exception):
pass
class NotFoundError(Exception):
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
from lxml import etree
from dateutil import parser
from pkg_resources import resource_string
......@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String
from .fields import Date
from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger(__name__)
......@@ -16,7 +18,7 @@ class FolditFields(object):
# default to what Spring_7012x uses
required_level = Integer(default=4, 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_leaderboard = String(scope=Scope.settings, default='false')
......@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule):
required_sublevel="3"
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):
"""
......@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
@classmethod
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')
return xml_object
class @Annotatable
_debug: false
# selectors for the annotatable xmodule
# selectors for the annotatable xmodule
wrapperSelector: '.annotatable-wrapper'
toggleAnnotationsSelector: '.annotatable-toggle-annotations'
toggleInstructionsSelector: '.annotatable-toggle-instructions'
instructionsSelector: '.annotatable-instructions'
......@@ -61,7 +62,7 @@ class @Annotatable
my: 'bottom center' # of tooltip
at: 'top center' # of target
target: $(el) # where the tooltip was triggered (i.e. the annotation span)
container: @$el
container: @$(@wrapperSelector)
adjust:
y: -5
show:
......@@ -75,6 +76,7 @@ class @Annotatable
classes: 'ui-tooltip-annotatable'
events:
show: @onShowTip
move: @onMoveTip
onClickToggleAnnotations: (e) => @toggleAnnotations()
......@@ -87,6 +89,55 @@ class @Annotatable
onShowTip: (event, api) =>
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) ->
problem_id = $(@problemReturnSelector).index(el)
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
......
......@@ -356,12 +356,12 @@ def remap_namespace(module, target_location_namespace):
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
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
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
if module.location.category == category:
my_metadata = dict(own_metadata(module))
for key in my_metadata.keys():
if key not in allowed:
if key not in _allowed:
err_cnt = err_cnt + 1
print ': found metadata on {0}. Studio will not support editing this piece of metadata, so it is not allowed. Metadata: {1} = {2}'. format(module.location.url(), key, my_metadata[key])
......
......@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this.
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
MAX_SCORE_ALLOWED = 50
......@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file:
<combinedopenended attempts="10000" max_score="1">
<combinedopenended attempts="10000">
<rubric>
Blah blah rubric.
</rubric>
......@@ -143,23 +139,19 @@ class CombinedOpenEndedV1Module():
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
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)
try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
self.timeinfo = TimeInfo(due_date, grace_period_string)
except:
log.error("Error parsing due date information in location {0}".format(location))
raise
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)
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
self.static_data = {
......
......@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message)
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)
success = rubric_dict['success']
rubric_feedback = rubric_dict['html']
......@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
log.error(error_message)
raise RubricParsingError(error_message)
if int(total) != int(max_score):
#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)
return int(total)
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
from datetime import datetime
from pkg_resources import resource_string
from .capa_module import ComplexEncoder
from .stringify import stringify_children
from .x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo
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
......@@ -28,13 +28,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
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)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, scope=Scope.settings)
display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings)
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)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
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)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
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):
......@@ -72,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
self._model_data['due'] = due_date
try:
self.timeinfo = TimeInfo(self.display_due_date_string, self.grace_period_string)
self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
except:
log.error("Error parsing due date information in location {0}".format(location))
raise
......
......@@ -30,8 +30,8 @@ class PollFields(object):
# Name of poll to use in links to this poll
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)
poll_answer = String(help="Student answer", scope=Scope.student_state, default='')
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.user_state, default='')
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=[])
......
......@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__)
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):
......
......@@ -23,7 +23,7 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student
# 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):
......
......@@ -2,12 +2,12 @@
metadata:
display_name: Open Ended Response
max_attempts: 1
max_score: 1
is_graded: False
version: 1
display_name: Open Ended Response
skip_spelling_checks: False
accept_file_upload: False
weight: ""
data: |
<combinedopenended>
<rubric>
......
......@@ -6,6 +6,7 @@ metadata:
link_to_location: None
is_graded: False
max_grade: 1
weight: ""
data: |
<peergrading>
</peergrading>
......
......@@ -5,11 +5,15 @@ import unittest
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.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore import Location
from lxml import etree
import capa.xqueue_interface as xqueue_interface
from datetime import datetime
import logging
log = logging.getLogger(__name__)
from . import test_system
......@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = test_system()
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):
......@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch="score_update"):
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}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
......@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase):
location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"])
definition_template = """
<combinedopenended attempts="10000">
{rubric}
{prompt}
<task>
{task1}
</task>
<task>
{task2}
</task>
</combinedopenended>
"""
prompt = "<prompt>This is a question prompt</prompt>"
rubric = '''<rubric><rubric>
<category>
......@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
</openendedparam>
</openended>'''
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):
self.test_system = test_system()
# TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
......@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = self.combinedoe.update_task_states()
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
from time import strptime
import datetime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
import xmodule.course_module
from xmodule.util.date_utils import time_to_datetime
ORG = 'test_org'
......@@ -39,6 +42,17 @@ class DummySystem(ImportSystem):
class IsNewCourseTestCase(unittest.TestCase):
"""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
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None):
"""Get a dummy course"""
......@@ -126,10 +140,7 @@ class IsNewCourseTestCase(unittest.TestCase):
print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2])
@patch('xmodule.course_module.time.gmtime')
def test_is_newish(self, gmtime_mock):
gmtime_mock.return_value = NOW
def test_is_newish(self):
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=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 -*-
from path import path
import unittest
from fs.memoryfs import MemoryFS
from lxml import etree
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.errortracker import make_error_tracker
from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.fields import Date
from .test_export import DATA_DIR
......@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase):
- inherited metadata doesn't leak to children.
"""
system = self.get_system()
v = '1 hour'
v = 'March 20 17:00'
url_name = 'test1'
start_xml = '''
<course org="{org}" course="{course}"
......@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase):
compute_inherited_metadata(descriptor)
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
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
resource_fs = MemoryFS()
......
import dateutil
import dateutil.parser
import datetime
from .timeparse import parse_timedelta
from xmodule.util.date_utils import time_to_datetime
import logging
log = logging.getLogger(__name__)
......@@ -9,7 +7,7 @@ log = logging.getLogger(__name__)
class TimeInfo(object):
"""
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:
self.display_due_date - the 'official' due date that gets displayed to students
......@@ -17,13 +15,10 @@ class TimeInfo(object):
self.close_date - the real due date
"""
def __init__(self, display_due_date_string, grace_period_string):
if display_due_date_string is not None:
try:
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
def __init__(self, due_date, grace_period_string):
if due_date is not None:
self.display_due_date = time_to_datetime(due_date)
else:
self.display_due_date = None
......
......@@ -16,9 +16,9 @@ log = logging.getLogger(__name__)
class TimeLimitFields(object):
beginning_at = Float(help="The time this timer was started", scope=Scope.student_state)
ending_at = Float(help="The time this timer will end", scope=Scope.student_state)
accomodation_code = String(help="A code indicating accommodations to be given the student", 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.user_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)
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)
......
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__)
class VideoFields(object):
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)
......
......@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
class VideoAlphaFields(object):
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)
......@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
else:
# VS[compat]
# 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', {
'youtube_streams': self.youtube_streams,
......
......@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# cdodge: this is a list of metadata names which are 'system' metadata
# 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
# be equal
......
......@@ -12,22 +12,49 @@
}
}
//NOTE:
// Genex uses six global functions:
// genexSetDNASequence (exported from GWT)
// genexSetClickEvent (exported from GWT)
// genexSetKeyEvent (exported from GWT)
// genexSetProblemNumber (exported from GWT)
// NOTE:
// Genex uses 8 global functions, all prefixed with genex:
// 6 are exported from GWT:
// genexSetInitialDNASequence
// genexSetDNASequence
// genexGetDNASequence
// genexSetClickEvent
// genexSetKeyEvent
// genexSetProblemNumber
//
// It calls genexIsReady with a deferred command when it has finished
// initialization and has drawn itself
// genexStoreAnswer(answer) is called when the GWT [Store Answer] button
// is clicked
// genexStoreAnswer(answer) is called each time the DNA sequence changes
// 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() {
//Load DNA sequence
var dna_sequence = $('#dna_sequence').val();
genexSetDNASequence(dna_sequence);
var input_field = genexGetInputField();
var genex_saved_state = input_field.val();
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
genexSetClickEvent();
genexSetKeyEvent();
......@@ -35,10 +62,9 @@
var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number);
};
genexStoreAnswer = function(ans) {
var problem = $('#genex_container').parents('.problem');
var input_field = problem.find('input[type="hidden"][name!="dna_sequence"][name!="genex_problem_number"]');
input_field.val(ans);
genexStoreAnswer = function(answer) {
var input_field = genexGetInputField();
var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer};
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 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}
......@@ -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 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)}}
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)}
genex();
\ No newline at end of file
......@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log
rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0
# Run the python unit tests
rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || 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
# rake phantomjs_jasmine_lms || true
# Run the jaavascript unit tests
rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
......
......@@ -73,6 +73,9 @@ class Command(BaseCommand):
ended_courses.append(course_id)
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)
enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related(
......@@ -99,6 +102,6 @@ class Command(BaseCommand):
student, course_id)['status'] in valid_statuses:
if not options['noop']:
# 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':
print '{0} - {1}'.format(student, ret)
......@@ -115,7 +115,7 @@ class XQueueCertInterface(object):
raise NotImplementedError
def add_cert(self, student, course_id):
def add_cert(self, student, course_id, course=None):
"""
Arguments:
......@@ -151,9 +151,12 @@ class XQueueCertInterface(object):
if cert_status in VALID_STATUSES:
# 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(
user=student, course_id=course_id)
......
......@@ -13,7 +13,6 @@ from xblock.core import Scope
from .module_render import get_module, get_module_for_descriptor
from xmodule import graders
from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score
from .models import StudentModule
......@@ -43,7 +42,6 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
else:
return descriptor.get_children()
stack = [descriptor]
while len(stack) > 0:
......@@ -66,7 +64,7 @@ def yield_problems(request, course, student):
).values_list('module_state_key', flat=True))
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:
section_descriptor = section['section_descriptor']
......@@ -123,7 +121,7 @@ def answer_distributions(request, course):
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
grade. The keys in the output are:
......@@ -158,10 +156,16 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=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%
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
key = LmsKeyValueStore.Key(
Scope.student_state,
Scope.user_state,
student.id,
moduledescriptor.location,
None
......@@ -174,7 +178,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
scores = []
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
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
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:
raw_scores += scores
else:
section_total = Score(0.0, 1.0, False, section_name)
graded_total = Score(0.0, 1.0, True, section_name)
#Add the graded total to totaled_scores
if graded_total.possible > 0:
format_scores.append(graded_total)
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
......@@ -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 forgo that, our arguments
# TODO: We need the request to pass into here. If we could forego that, our arguments
# would be simpler
course_module = get_module(student, request,
course.location, model_data_cache,
course.id, depth=None)
course_module = get_module(student, request, course.location, model_data_cache, course.id, depth=None)
if not course_module:
# This student must not have access to the course.
return None
......@@ -310,20 +312,19 @@ def progress_summary(student, request, course, model_data_cache):
if correct is None and total is None:
continue
scores.append(Score(correct, total, graded,
module_descriptor.display_name_with_default))
scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default))
scores.reverse()
section_total, graded_total = graders.aggregate_scores(
section_total, _ = graders.aggregate_scores(
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({
'display_name': section_module.display_name_with_default,
'url_name': section_module.url_name,
'scores': scores,
'section_total': section_total,
'format': format,
'format': module_format,
'due': section_module.lms.due,
'graded': graded,
})
......@@ -353,11 +354,13 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if not user.is_authenticated():
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:
problem = module_creator(problem_descriptor)
d = problem.get_score()
if d is not None:
return (d['score'], d['total'])
score = problem.get_score()
if score is not None:
return (score['score'], score['total'])
else:
return (None, None)
......@@ -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
key = LmsKeyValueStore.Key(
Scope.student_state,
Scope.user_state,
user.id,
problem_descriptor.location,
None
......@@ -394,7 +397,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
if total is None:
return (None, None)
#Now we re-weight the problem, if specified
# Now we re-weight the problem, if specified
weight = problem_descriptor.weight
if weight is not None:
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):
"""
if scope in (Scope.children, Scope.parent):
return []
elif scope == Scope.student_state:
elif scope == Scope.user_state:
return self._chunked_query(
StudentModule,
'module_state_key__in',
......@@ -159,7 +159,7 @@ class ModelDataCache(object):
),
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.student_preferences:
elif scope == Scope.preferences:
return self._chunked_query(
XModuleStudentPrefsField,
'module_type__in',
......@@ -167,7 +167,7 @@ class ModelDataCache(object):
student=self.user.pk,
field_name__in=set(field.name for field in fields),
)
elif scope == Scope.student_info:
elif scope == Scope.user_info:
return self._query(
XModuleStudentInfoField,
student=self.user.pk,
......@@ -190,15 +190,15 @@ class ModelDataCache(object):
"""
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())
elif key.scope == Scope.content:
return (key.scope, key.block_scope_id.url(), key.field_name)
elif key.scope == Scope.settings:
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)
elif key.scope == Scope.student_info:
elif key.scope == Scope.user_info:
return (key.scope, key.field_name)
def _cache_key_from_field_object(self, scope, field_object):
......@@ -206,15 +206,15 @@ class ModelDataCache(object):
Return the key used in the ModelDataCache for the specified scope and
field
"""
if scope == Scope.student_state:
if scope == Scope.user_state:
return (scope, field_object.module_state_key)
elif scope == Scope.content:
return (scope, field_object.definition_id, field_object.field_name)
elif scope == Scope.settings:
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)
elif scope == Scope.student_info:
elif scope == Scope.user_info:
return (scope, field_object.field_name)
def find(self, key):
......@@ -237,7 +237,7 @@ class ModelDataCache(object):
if field_object is not None:
return field_object
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
field_object, _ = StudentModule.objects.get_or_create(
course_id=self.course_id,
student=self.user,
......@@ -255,13 +255,13 @@ class ModelDataCache(object):
field_name=key.field_name,
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_name=key.field_name,
module_type=key.block_scope_id,
student=self.user,
)
elif key.scope == Scope.student_info:
elif key.scope == Scope.user_info:
field_object, _ = XModuleStudentInfoField.objects.get_or_create(
field_name=key.field_name,
student=self.user,
......@@ -281,12 +281,12 @@ class LmsKeyValueStore(KeyValueStore):
If the scope to write to is not one of the 5 named scopes:
Scope.content
Scope.settings
Scope.student_state
Scope.student_preferences
Scope.student_info
Scope.user_state
Scope.preferences
Scope.user_info
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
scope involved and have the field name as a key
......@@ -297,9 +297,9 @@ class LmsKeyValueStore(KeyValueStore):
_allowed_scopes = (
Scope.content,
Scope.settings,
Scope.student_state,
Scope.student_preferences,
Scope.student_info,
Scope.user_state,
Scope.preferences,
Scope.user_info,
Scope.children,
)
......@@ -321,7 +321,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
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]
else:
return json.loads(field_object.value)
......@@ -335,7 +335,7 @@ class LmsKeyValueStore(KeyValueStore):
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
state = json.loads(field_object.state)
state[key.field_name] = value
field_object.state = json.dumps(state)
......@@ -355,7 +355,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
raise KeyError(key.field_name)
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
state = json.loads(field_object.state)
del state[key.field_name]
field_object.state = json.dumps(state)
......@@ -377,7 +377,7 @@ class LmsKeyValueStore(KeyValueStore):
if field_object is None:
return False
if key.scope == Scope.student_state:
if key.scope == Scope.user_state:
return key.field_name in json.loads(field_object.state)
else:
return True
......
......@@ -165,7 +165,7 @@ class XModuleSettingsField(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:
......@@ -199,7 +199,7 @@ class XModuleStudentPrefsField(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:
......
......@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
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 requests.auth import HTTPBasicAuth
......@@ -23,7 +23,7 @@ from .models import StudentModule
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import unique_id_for_user
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.django import modulestore
from xmodule.x_module import ModuleSystem
......@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id):
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, p)
# If we can't find the module, respond with a 404
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
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:
log.exception("error processing ajax call")
raise
......
......@@ -32,9 +32,9 @@ course_id = 'edX/test_course/test'
content_key = partial(LmsKeyValueStore.Key, Scope.content, 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'))
student_prefs_key = partial(LmsKeyValueStore.Key, Scope.student_preferences, 'user', 'problem')
student_info_key = partial(LmsKeyValueStore.Key, Scope.student_info, 'user', None)
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
class UserFactory(factory.Factory):
......@@ -115,13 +115,13 @@ class TestInvalidScopes(TestCase):
def setUp(self):
self.desc_md = {}
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)
def test_invalid_scopes(self):
for scope in (Scope(student=True, block=BlockScope.DEFINITION),
Scope(student=False, block=BlockScope.TYPE),
Scope(student=False, block=BlockScope.ALL)):
for scope in (Scope(user=True, block=BlockScope.DEFINITION),
Scope(user=False, block=BlockScope.TYPE),
Scope(user=False, block=BlockScope.ALL)):
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.delete, LmsKeyValueStore.Key(scope, None, None, 'field'))
......@@ -134,48 +134,48 @@ class TestStudentModuleStorage(TestCase):
self.desc_md = {}
student_module = StudentModuleFactory(state=json.dumps({'a_field': 'a_value'}))
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)
def test_get_existing_field(self):
"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):
"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):
"Test that setting an existing student_state field changes the value"
self.kvs.set(student_state_key('a_field'), 'new_value')
"Test that setting an existing user_state field changes the value"
self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_set_missing_field(self):
"Test that setting a new student_state field changes the value"
self.kvs.set(student_state_key('not_a_field'), 'new_value')
"Test that setting a new user_state field changes the value"
self.kvs.set(user_state_key('not_a_field'), 'new_value')
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))
def test_delete_existing_field(self):
"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.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):
"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({'a_field': 'a_value'}, json.loads(StudentModule.objects.all()[0].state))
def test_has_existing_field(self):
"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):
"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):
......@@ -187,14 +187,14 @@ class TestMissingStudentModule(TestCase):
def test_get_field_from_missing_student_module(self):
"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):
"Test that setting a field in a missing StudentModule creates the student module"
self.assertEquals(0, len(self.mdc.cache))
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, StudentModule.objects.all().count())
......@@ -207,11 +207,11 @@ class TestMissingStudentModule(TestCase):
def test_delete_field_from_missing_student_module(self):
"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):
"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):
......@@ -286,13 +286,13 @@ class TestContentStorage(StorageTestBase, TestCase):
class TestStudentPrefsStorage(StorageTestBase, TestCase):
factory = StudentPrefsFactory
scope = Scope.student_preferences
key_factory = student_prefs_key
scope = Scope.preferences
key_factory = prefs_key
storage_class = XModuleStudentPrefsField
class TestStudentInfoStorage(StorageTestBase, TestCase):
factory = StudentInfoFactory
scope = Scope.student_info
key_factory = student_info_key
scope = Scope.user_info
key_factory = user_info_key
storage_class = XModuleStudentInfoField
......@@ -124,17 +124,17 @@ class TestTOC(TestCase):
expected = ([{'active': True, 'sections':
[{'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,
'format': '', 'due': '', 'active': False},
'format': '', 'due': None, 'active': False},
{'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,
'format': '', 'due': '', 'active': False}],
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'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'}])
actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache)
......@@ -151,17 +151,17 @@ class TestTOC(TestCase):
expected = ([{'active': True, 'sections':
[{'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,
'format': '', 'due': '', 'active': True},
'format': '', 'due': None, 'active': 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,
'format': '', 'due': '', 'active': False}],
'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'},
{'active': False, 'sections':
[{'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'}])
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):
'courseware_summary': courseware_summary,
'grade_summary': grade_summary,
'staff_access': staff_access,
'student': student,
}
context.update()
......@@ -663,13 +664,13 @@ def submission_history(request, course_id, student_username, location):
.format(student_username, location))
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 not history_entries:
student_module.save()
history_entries = StudentModuleHistory.objects \
.filter(student_module=student_module).order_by('-created')
.filter(student_module=student_module).order_by('-id')
context = {
'history_entries': history_entries,
......
......@@ -6,7 +6,8 @@ Enrollments.
"""
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):
......
......@@ -6,7 +6,8 @@ Enrollments.
"""
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):
......
"""
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):
# All the not-actually-in-the-course hw and labs come from the
# 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"
"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"
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"
'''
self.assertEqual(body, expected_body, msg)
......
......@@ -229,9 +229,11 @@ def instructor_dashboard(request, course_id):
if student_to_reset is not None:
# 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:
(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,
course_id=course_id,
module_state_key=module_state_key)
......
......@@ -15,7 +15,6 @@ from scipy.optimize import curve_fit
from django.conf import settings
from django.db.models import Sum, Max
from psychometrics.models import *
from xmodule.modulestore import Location
log = logging.getLogger("mitx.psychometrics")
......@@ -246,13 +245,16 @@ def generate_plots_for_problem(problem):
yset['ydat'] = ydat
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])
yset['fitparam'] = cfp
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
fitx = np.linspace(xdat[0], xdat[-1], 100)
yset['fitx'] = fitx
yset['fity'] = func_2pl(np.array(fitx), *cfp[0])
try:
cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0])
yset['fitparam'] = cfp
yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0])
yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])]
fitx = np.linspace(xdat[0], xdat[-1], 100)
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
......@@ -289,7 +291,7 @@ def generate_plots_for_problem(problem):
'info': '',
'data': jsdata,
'cmd': '[%s], %s' % (','.join(jsplots), axisopts),
})
})
#log.debug('plots = %s' % plots)
return msg, plots
......@@ -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
the PsychometricsData instance for the given StudentModule instance.
"""
sm = studentmodule.objects.get_or_create(
course_id=course_id,
student=user,
module_state_key=module_state_key,
defaults={'state': '{}', 'module_type': 'problem'},
)
sm, status = StudentModule.objects.get_or_create(
course_id=course_id,
student=user,
module_state_key=module_state_key,
defaults={'state': '{}', 'module_type': 'problem'},
)
try:
pmd = PsychometricData.objects.using(db).get(studentmodule=sm)
......@@ -329,7 +331,11 @@ def make_psychometrics_data_update_handler(course_id, user, module_state_key):
return
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:
checktimes = eval(pmd.checktimes) # update log of attempt timestamps
except:
......
......@@ -221,7 +221,7 @@ FILE_UPLOAD_HANDLERS = (
########################### 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 ###########################
MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True
......
<%! from django.core.urlresolvers import reverse %>
<%! from xmodule.util.date_utils import get_default_time_display %>
<%def name="make_chapter(chapter)">
<div class="chapter">
......@@ -10,7 +11,7 @@
<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']])}">
<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>
</li>
% endfor
......
......@@ -190,6 +190,7 @@ function goto( mode)
<input type="submit" name="action" value="Export CSV file of grades for assignment">
</li>
</ul>
<hr width="40%" style="align:left">
%endif
......@@ -197,11 +198,13 @@ function goto( mode)
<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>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:
<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>
<p>To delete the state of other XBlocks specify modulename/urlname, eg
<tt>combinedopenended/Humanities_SA_Peer</tt></p>
%endif
%endif
......
......@@ -13,6 +13,8 @@
from django.core.urlresolvers import reverse
%>
<%! from xmodule.util.date_utils import get_default_time_display %>
<%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.stack.js')}"></script>
......@@ -29,7 +31,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<section class="course-info">
<header>
<h1>Course Progress</h1>
<h1>Course Progress for Student '${student.username}' (${student.email})</h1>
</header>
%if not course.disable_progress_graph:
......@@ -60,9 +62,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<p>
${section['format']}
%if 'due' in section and section['due']!="":
%if section.get('due') is not None:
<em>
due ${section['due']}
due ${get_default_time_display(section['due'])}
</em>
%endif
</p>
......
<%!
from xmodule.util.date_utils import get_default_time_display
%>
<div class="folditbasic">
<p><strong>Due:</strong> ${due}
<p><strong>Due:</strong> ${get_default_time_display(due)}
<p>
<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