Commit f3be02d7 by Diana Huang

Merge branch 'master' into diana/rubric-ui-improvements

parents 7860e013 ecce3e57
...@@ -261,7 +261,6 @@ def edit_unit(request, location): ...@@ -261,7 +261,6 @@ def edit_unit(request, location):
break break
lms_link = get_lms_link_for_item(item.location) lms_link = get_lms_link_for_item(item.location)
preview_lms_link = get_lms_link_for_item(item.location, preview=True)
component_templates = defaultdict(list) component_templates = defaultdict(list)
......
...@@ -80,64 +80,6 @@ $(document).ready(function() { ...@@ -80,64 +80,6 @@ $(document).ready(function() {
$('.import .file-input').click(); $('.import .file-input').click();
}); });
// making the unit list draggable. Note: sortable didn't work b/c it considered
// drop points which the user hovered over as destinations and proactively changed
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
// point was the last dom change.
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid"
});
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid"
});
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
handle: 'header .drag-handle',
stack: '.courseware-section',
revert: "invalid"
});
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
tolerance: "pointer",
hoverClass: "dropover",
drop: onUnitReordered
});
$('.subsection-list > ol').droppable({
// why don't we have a more useful class for subsections than id-holder?
accept : '.id-holder', // '.unit, .id-holder',
tolerance: "pointer",
hoverClass: "dropover",
drop: onSubsectionReordered,
greedy: true
});
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
tolerance: "pointer",
drop: onSectionReordered,
greedy: true
});
$('.new-course-button').bind('click', addNewCourse); $('.new-course-button').bind('click', addNewCourse);
// section name editing // section name editing
...@@ -279,136 +221,6 @@ function removePolicyMetadata(e) { ...@@ -279,136 +221,6 @@ function removePolicyMetadata(e) {
saveSubsection() saveSubsection()
} }
CMS.HesitateEvent.toggleXpandHesitation = null;
function initiateHesitate(event, ui) {
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
$('.collapsed').each(function() {
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
// reset b/c these were holding values from aborts
this.isover = false;
});
}
function checkHoverState(event, ui) {
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
var draggable = $(this).data("ui-draggable"),
x1 = (draggable.positionAbs || draggable.position.absolute).left + (draggable.helperProportions.width / 2),
y1 = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
$('.collapsed').each(function() {
// don't expand the thing being carried
if (ui.helper.is(this)) {
return;
}
$.extend(this, {offset : $(this).offset()});
var droppable = this,
l = droppable.offset.left,
r = l + droppable.proportions.width,
t = droppable.offset.top,
b = t + droppable.proportions.height;
if (l === r) {
// probably wrong values b/c invisible at the time of caching
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
r = l + droppable.proportions.width;
b = t + droppable.proportions.height;
}
// equivalent to the intersects test
var intersects = (l < x1 && // Right Half
x1 < r && // Left Half
t < y1 && // Bottom Half
y1 < b ), // Top Half
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
if(!c) {
return;
}
this[c] = true;
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
}
function removeHesitate(event, ui) {
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
CMS.HesitateEvent.toggleXpandHesitation = null;
}
function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
function onSectionReordered(event, ui) {
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
_handleReorder(event, ui, 'course-id', '.courseware-section');
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// if new to this parent, figure out which parent to remove it from and do so
if (!_.contains(children, ui.draggable.data('id'))) {
var old_parent = ui.draggable.parent();
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
old_children = _.without(old_children, ui.draggable.data('id'));
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
});
}
else {
// staying in same parent
// remove so that the replacement in the right place doesn't double it
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id'));
break;
}
}
// see if it goes at end (the above loop didn't insert it)
if (!_.contains(children, ui.draggable.data('id'))) {
$(event.target).append(ui.draggable);
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
children.push(ui.draggable.data('id'));
}
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
});
}
function getEdxTimeFromDateTimeVals(date_val, time_val, format) { function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
var edxTimeStr = null; var edxTimeStr = null;
......
$(document).ready(function() {
// making the unit list draggable. Note: sortable didn't work b/c it considered
// drop points which the user hovered over as destinations and proactively changed
// the dom; so, if the user subsequently dropped at an illegal spot, the reversion
// point was the last dom change.
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
zIndex: 999,
start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future
drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
zIndex: 999,
start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
handle: 'header .drag-handle',
stack: '.courseware-section',
revert: "invalid"
});
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
tolerance: "pointer",
hoverClass: "dropover",
drop: onUnitReordered
});
$('.subsection-list > ol').droppable({
// why don't we have a more useful class for subsections than id-holder?
accept : '.id-holder', // '.unit, .id-holder',
tolerance: "pointer",
hoverClass: "dropover",
drop: onSubsectionReordered,
greedy: true
});
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
tolerance: "pointer",
drop: onSectionReordered,
greedy: true
});
});
CMS.HesitateEvent.toggleXpandHesitation = null;
function initiateHesitate(event, ui) {
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
$('.collapsed, .unit, .id-holder').each(function() {
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
// reset b/c these were holding values from aborts
this.isover = false;
});
}
function computeIntersection(droppable, uiHelper, y) {
/*
* Test whether y falls within the bounds of the droppable on the Y axis
*/
// NOTE: this only judges y axis intersection b/c that's all we're doing right now
// don't expand the thing being carried
if (uiHelper.is(droppable)) {
return null;
}
$.extend(droppable, {offset : $(droppable).offset()});
var t = droppable.offset.top,
b = t + droppable.proportions.height;
if (t === b) {
// probably wrong values b/c invisible at the time of caching
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
b = t + droppable.proportions.height;
}
// equivalent to the intersects test
return (t < y && // Bottom Half
y < b ); // Top Half
}
// NOTE: selectorsToShove is not currently being used but I left this code as it did work but not well
function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
return function(event, ui) {
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
var draggable = $(this).data("ui-draggable"),
centerY = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
$(selectorsToOpen).each(function() {
var intersects = computeIntersection(this, ui.helper, centerY),
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
if(!c) {
return;
}
this[c] = true;
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
$(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) {
console.log('not up', $(this).data('id'));
$(this).removeClass('ui-dragging-pushup');
}
}
else if (intersectsBottom) {
console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup');
}
var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) {
console.log('not down', $(this).data('id'));
$(this).removeClass('ui-dragging-pushdown');
}
}
else if (intersectsTop) {
console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown');
}
});
}
}
function removeHesitate(event, ui) {
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
$('.ui-dragging-pushdown').removeClass('ui-dragging-pushdown');
$('.ui-dragging-pushup').removeClass('ui-dragging-pushup');
CMS.HesitateEvent.toggleXpandHesitation = null;
}
function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
// figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
function onSectionReordered(event, ui) {
// a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order)
_handleReorder(event, ui, 'course-id', '.courseware-section');
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
// if new to this parent, figure out which parent to remove it from and do so
if (!_.contains(children, ui.draggable.data('id'))) {
var old_parent = ui.draggable.parent();
var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
old_children = _.without(old_children, ui.draggable.data('id'));
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
});
}
else {
// staying in same parent
// remove so that the replacement in the right place doesn't double it
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id'));
break;
}
}
// see if it goes at end (the above loop didn't insert it)
if (!_.contains(children, ui.draggable.data('id'))) {
$(event.target).append(ui.draggable);
ui.draggable.attr("style", "position:relative;"); // STYLE hack too
children.push(ui.draggable.data('id'));
}
$.ajax({
url: "/save_item",
type: "POST",
dataType: "json",
contentType: "application/json",
data:JSON.stringify({ 'id' : subsection_id, 'children' : children})
});
}
...@@ -647,11 +647,29 @@ input.courseware-unit-search-input { ...@@ -647,11 +647,29 @@ input.courseware-unit-search-input {
// sort/drag and drop // sort/drag and drop
.ui-droppable { .ui-droppable {
@include transition (padding 0.5s ease-in-out 0s);
min-height: 20px; min-height: 20px;
padding: 0;
&.dropover { &.dropover {
padding-top: 10px; padding: 15px 0;
padding-bottom: 10px; }
}
.ui-draggable-dragging {
@include box-shadow(0 1px 2px rgba(0, 0, 0, .3));
border: 1px solid $darkGrey;
opacity : 0.2;
&:hover {
opacity : 1.0;
.section-item {
background: $yellow !important;
}
}
// hiding unit button - temporary fix until this semantically corrected
.new-unit-item {
display: none;
} }
} }
...@@ -659,5 +677,3 @@ ol.ui-droppable .branch:first-child .section-item { ...@@ -659,5 +677,3 @@ ol.ui-droppable .branch:first-child .section-item {
border-top: none; border-top: none;
} }
...@@ -305,6 +305,7 @@ ...@@ -305,6 +305,7 @@
.wrapper-component-editor { .wrapper-component-editor {
z-index: 9999; z-index: 9999;
position: relative; position: relative;
background: $lightBluishGrey2;
} }
.component-editor { .component-editor {
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -40,6 +40,11 @@ def get_logger_config(log_dir, ...@@ -40,6 +40,11 @@ def get_logger_config(log_dir,
if console_loglevel is None or console_loglevel not in LOG_LEVELS: if console_loglevel is None or console_loglevel not in LOG_LEVELS:
console_loglevel = 'DEBUG' if debug else 'INFO' console_loglevel = 'DEBUG' if debug else 'INFO'
if service_variant is None:
# default to a blank string so that if SERVICE_VARIANT is not
# set we will not log to a sub directory
service_variant = ''
hostname = platform.node().split(".")[0] hostname = platform.node().split(".")[0]
syslog_format = ("[service_variant={service_variant}]" syslog_format = ("[service_variant={service_variant}]"
"[%(name)s][env:{logging_env}] %(levelname)s " "[%(name)s][env:{logging_env}] %(levelname)s "
......
...@@ -20,6 +20,7 @@ setup( ...@@ -20,6 +20,7 @@ setup(
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor", "book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor",
"combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor", "combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor",
"conditional = xmodule.conditional_module:ConditionalDescriptor",
"course = xmodule.course_module:CourseDescriptor", "course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
...@@ -28,6 +29,7 @@ setup( ...@@ -28,6 +29,7 @@ setup(
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
"problem = xmodule.capa_module:CapaDescriptor", "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
......
...@@ -2,6 +2,7 @@ import cgi ...@@ -2,6 +2,7 @@ import cgi
import datetime import datetime
import dateutil import dateutil
import dateutil.parser import dateutil.parser
import hashlib
import json import json
import logging import logging
import traceback import traceback
...@@ -25,6 +26,22 @@ log = logging.getLogger("mitx.courseware") ...@@ -25,6 +26,22 @@ log = logging.getLogger("mitx.courseware")
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
# Generated this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20
def randomization_bin(seed, problem_id):
"""
Pick a randomization bin for the problem given the user's seed and a problem id.
We do this because we only want e.g. 20 randomizations of a problem to make analytics
interesting. To avoid having sets of students that always get the same problems,
we'll combine the system's per-student seed with the problem id in picking the bin.
"""
h = hashlib.sha1()
h.update(str(seed))
h.update(str(problem_id))
# get the first few digits of the hash, convert to an int, then mod.
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
def only_one(lst, default="", process=lambda x: x): def only_one(lst, default="", process=lambda x: x):
""" """
...@@ -138,13 +155,9 @@ class CapaModule(XModule): ...@@ -138,13 +155,9 @@ class CapaModule(XModule):
if self.rerandomize == 'never': if self.rerandomize == 'never':
self.seed = 1 self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'): elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# TODO: This line is badly broken: # see comment on randomization_bin
# (1) We're passing student ID to xmodule. self.seed = randomization_bin(system.seed, self.location.url)
# (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
# to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
# - analytics really needs small number of bins.
self.seed = system.id
else: else:
self.seed = None self.seed = None
...@@ -389,38 +402,54 @@ class CapaModule(XModule): ...@@ -389,38 +402,54 @@ class CapaModule(XModule):
}) })
return json.dumps(d, cls=ComplexEncoder) return json.dumps(d, cls=ComplexEncoder)
def is_past_due(self):
"""
Is it now past this problem's due date, including grace period?
"""
return (self.close_date is not None and
datetime.datetime.utcnow() > self.close_date)
def closed(self): def closed(self):
''' Is the student still allowed to submit answers? ''' ''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts: if self.attempts == self.max_attempts:
return True return True
if self.close_date is not None and datetime.datetime.utcnow() > self.close_date: if self.is_past_due():
return True return True
return False return False
def is_completed(self):
# used by conditional module
# return self.answer_available()
return self.lcp.done
def is_attempted(self):
# used by conditional module
return self.attempts > 0
def answer_available(self): def answer_available(self):
''' Is the user allowed to see an answer? '''
Is the user allowed to see an answer?
''' '''
if self.show_answer == '': if self.show_answer == '':
return False return False
elif self.show_answer == "never":
if self.show_answer == "never":
return False return False
elif self.system.user_is_staff:
# Admins can see the answer, unless the problem explicitly prevents it # This is after the 'never' check because admins can see the answer
if self.system.user_is_staff: # unless the problem explicitly prevents it
return True return True
elif self.show_answer == 'attempted':
if self.show_answer == 'attempted':
return self.attempts > 0 return self.attempts > 0
elif self.show_answer == 'answered':
if self.show_answer == 'answered': # NOTE: this is slightly different from 'attempted' -- resetting the problems
# makes lcp.done False, but leaves attempts unchanged.
return self.lcp.done return self.lcp.done
elif self.show_answer == 'closed':
if self.show_answer == 'closed':
return self.closed() return self.closed()
elif self.show_answer == 'past_due':
if self.show_answer == 'always': return self.is_past_due()
elif self.show_answer == 'always':
return True return True
return False return False
......
import json
import logging
from xmodule.x_module import XModule
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from pkg_resources import resource_string
log = logging.getLogger('mitx.' + __name__)
class ConditionalModule(XModule):
'''
Blocks child module from showing unless certain conditions are met.
Example:
<conditional condition="require_completed" required="tag/url_name1&tag/url_name2">
<video url_name="secret_video" />
</conditional>
<conditional condition="require_attempted" required="tag/url_name1&tag/url_name2">
<video url_name="secret_video" />
</conditional>
'''
js = {'coffee': [resource_string(__name__, 'js/src/conditional/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "Conditional"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
"""
In addition to the normal XModule init, provide:
self.condition = string describing condition required
"""
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
self.contents = None
self.condition = self.metadata.get('condition','')
#log.debug('conditional module required=%s' % self.required_modules_list)
def _get_required_modules(self):
self.required_modules = []
for descriptor in self.descriptor.get_required_module_descriptors():
module = self.system.get_module(descriptor)
self.required_modules.append(module)
#log.debug('required_modules=%s' % (self.required_modules))
def is_condition_satisfied(self):
self._get_required_modules()
if self.condition=='require_completed':
# all required modules must be completed, as determined by
# the modules .is_completed() method
for module in self.required_modules:
#log.debug('in is_condition_satisfied; student_answers=%s' % module.lcp.student_answers)
#log.debug('in is_condition_satisfied; instance_state=%s' % module.instance_state)
if not hasattr(module, 'is_completed'):
raise Exception('Error in conditional module: required module %s has no .is_completed() method' % module)
if not module.is_completed():
log.debug('conditional module: %s not completed' % module)
return False
else:
log.debug('conditional module: %s IS completed' % module)
return True
elif self.condition=='require_attempted':
# all required modules must be attempted, as determined by
# the modules .is_attempted() method
for module in self.required_modules:
if not hasattr(module, 'is_attempted'):
raise Exception('Error in conditional module: required module %s has no .is_attempted() method' % module)
if not module.is_attempted():
log.debug('conditional module: %s not attempted' % module)
return False
else:
log.debug('conditional module: %s IS attempted' % module)
return True
else:
raise Exception('Error in conditional module: unknown condition "%s"' % self.condition)
return True
def get_html(self):
self.is_condition_satisfied()
return self.system.render_template('conditional_ajax.html', {
'element_id': self.location.html_id(),
'id': self.id,
'ajax_url': self.system.ajax_url,
})
def handle_ajax(self, dispatch, post):
'''
This is called by courseware.module_render, to handle an AJAX call.
'''
#log.debug('conditional_module handle_ajax: dispatch=%s' % dispatch)
if not self.is_condition_satisfied():
context = {'module': self}
html = self.system.render_template('conditional_module.html', context)
return json.dumps({'html': html})
if self.contents is None:
self.contents = [child.get_html() for child in self.get_display_items()]
# for now, just deal with one child
html = self.contents[0]
return json.dumps({'html': html})
class ConditionalDescriptor(SequenceDescriptor):
module_class = ConditionalModule
filename_extension = "xml"
stores_state = True
has_score = False
def __init__(self, *args, **kwargs):
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
required_module_list = [tuple(x.split('/',1)) for x in self.metadata.get('required','').split('&')]
self.required_module_locations = []
for (tag, name) in required_module_list:
loc = self.location.dict()
loc['category'] = tag
loc['name'] = name
self.required_module_locations.append(Location(loc))
log.debug('ConditionalDescriptor required_module_locations=%s' % self.required_module_locations)
def get_required_module_descriptors(self):
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
not children of this module"""
return [self.system.load_item(loc) for loc in self.required_module_locations]
...@@ -401,11 +401,12 @@ section.open-ended-child { ...@@ -401,11 +401,12 @@ section.open-ended-child {
margin: 10px; margin: 10px;
} }
span.short-form-response { div.short-form-response {
padding: 9px;
background: #F6F6F6; background: #F6F6F6;
border: 1px solid #ddd; border: 1px solid #ddd;
margin-bottom: 20px; margin-bottom: 20px;
overflow-y: auto;
height: 200px;
@include clearfix; @include clearfix;
} }
......
...@@ -362,7 +362,7 @@ class @CombinedOpenEnded ...@@ -362,7 +362,7 @@ class @CombinedOpenEnded
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) => $.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
if response.state == "done" or response.state=="post_assessment" if response.state == "done" or response.state=="post_assessment"
delete window.queuePollerID delete window.queuePollerID
@reload location.reload()
else else
window.queuePollerID = window.setTimeout(@poll, 10000) window.queuePollerID = window.setTimeout(@poll, 10000)
...@@ -384,7 +384,7 @@ class @CombinedOpenEnded ...@@ -384,7 +384,7 @@ class @CombinedOpenEnded
answer_id = @answer_area.attr('id') answer_id = @answer_area.attr('id')
answer_val = @answer_area.val() answer_val = @answer_area.val()
new_text = '' new_text = ''
new_text = "<span class='#{answer_class}' id='#{answer_id}'>#{answer_val}</span>" new_text = "<div class='#{answer_class}' id='#{answer_id}'>#{answer_val}</div>"
@answer_area.replaceWith(new_text) @answer_area.replaceWith(new_text)
# wrap this so that it can be mocked # wrap this so that it can be mocked
......
class @Conditional
constructor: (element) ->
@el = $(element).find('.conditional-wrapper')
@id = @el.data('problem-id')
@element_id = @el.attr('id')
@url = @el.data('url')
@render()
$: (selector) ->
$(selector, @el)
updateProgress: (response) =>
if response.progress_changed
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
render: (content) ->
if content
@el.html(content)
XModule.loadModules(@el)
else
$.postWithPrefix "#{@url}/conditional_get", (response) =>
@el.html(response.html)
XModule.loadModules(@el)
...@@ -50,7 +50,7 @@ class @HTMLEditingDescriptor ...@@ -50,7 +50,7 @@ class @HTMLEditingDescriptor
}) })
@showingVisualEditor = true @showingVisualEditor = true
@element.on('click', '.editor-tabs .tab', @onSwitchEditor) @element.on('click', '.editor-tabs .tab', this, @onSwitchEditor)
@setupTinyMCE: (ed) -> @setupTinyMCE: (ed) ->
ed.addButton('wrapAsCode', { ed.addButton('wrapAsCode', {
...@@ -71,15 +71,18 @@ class @HTMLEditingDescriptor ...@@ -71,15 +71,18 @@ class @HTMLEditingDescriptor
e.preventDefault(); e.preventDefault();
if not $(e.currentTarget).hasClass('current') if not $(e.currentTarget).hasClass('current')
$('.editor-tabs .current', @element).removeClass('current') element = e.data.element
$(e.currentTarget).addClass('current') $(e.currentTarget).addClass('current')
$('table.mceToolbar', @element).toggleClass(HTMLEditingDescriptor.isInactiveClass) $(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass)
$(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass) $(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass)
visualEditor = @getVisualEditor() visualEditor = @getVisualEditor(element)
if $(e.currentTarget).attr('data-tab') is 'visual' if $(e.currentTarget).attr('data-tab') is 'visual'
$(element).find('.html-tab').removeClass('current')
@showVisualEditor(visualEditor) @showVisualEditor(visualEditor)
else else
$(element).find('.visual-tab').removeClass('current')
@showAdvancedEditor(visualEditor) @showAdvancedEditor(visualEditor)
# Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing. # Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
...@@ -104,17 +107,17 @@ class @HTMLEditingDescriptor ...@@ -104,17 +107,17 @@ class @HTMLEditingDescriptor
focusVisualEditor: (visualEditor) -> focusVisualEditor: (visualEditor) ->
visualEditor.focus() visualEditor.focus()
getVisualEditor: -> getVisualEditor: (element) ->
### ###
Returns the instance of TinyMCE. Returns the instance of TinyMCE.
This is different from the textarea that exists in the HTML template (@tiny_mce_textarea. This is different from the textarea that exists in the HTML template (@tiny_mce_textarea.
### ###
return tinyMCE.get($('.tiny-mce', this.element).attr('id')) return tinyMCE.get($(element).find('.tiny-mce').attr('id'))
save: -> save: ->
@element.off('click', '.editor-tabs .tab', @onSwitchEditor) @element.off('click', '.editor-tabs .tab', @onSwitchEditor)
text = @advanced_editor.getValue() text = @advanced_editor.getValue()
visualEditor = @getVisualEditor() visualEditor = @getVisualEditor(@element)
if @showingVisualEditor and visualEditor.isDirty() if @showingVisualEditor and visualEditor.isDirty()
text = visualEditor.getContent({no_events: 1}) text = visualEditor.getContent({no_events: 1})
data: text data: text
import json
import logging
import random
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor
from pkg_resources import resource_string
log = logging.getLogger('mitx.' + __name__)
class RandomizeModule(XModule):
"""
Chooses a random child module. Chooses the same one every time for each student.
Example:
<randomize>
<problem url_name="problem1" />
<problem url_name="problem2" />
<problem url_name="problem3" />
</randomize>
User notes:
- If you're randomizing amongst graded modules, each of them MUST be worth the same
number of points. Otherwise, the earth will be overrun by monsters from the
deeps. You have been warned.
Technical notes:
- There is more dark magic in this code than I'd like. The whole varying-children +
grading interaction is a tangle between super and subclasses of descriptors and
modules.
"""
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
# NOTE: calling self.get_children() creates a circular reference--
# it calls get_child_descriptors() internally, but that doesn't work until
# we've picked a choice
num_choices = len(self.descriptor.get_children())
self.choice = None
if instance_state is not None:
state = json.loads(instance_state)
self.choice = state.get('choice', None)
if self.choice > num_choices:
# Oops. Children changed. Reset.
self.choice = None
if self.choice is None:
# choose one based on the system seed, or randomly if that's not available
if num_choices > 0:
if system.seed is not None:
self.choice = system.seed % num_choices
else:
self.choice = random.randrange(0, num_choices)
if self.choice is not None:
self.child_descriptor = self.descriptor.get_children()[self.choice]
# Now get_children() should return a list with one element
log.debug("children of randomize module (should be only 1): %s",
self.get_children())
self.child = self.get_children()[0]
else:
self.child_descriptor = None
self.child = None
def get_instance_state(self):
return json.dumps({'choice': self.choice})
def get_child_descriptors(self):
"""
For grading--return just the chosen child.
"""
if self.child_descriptor is None:
return []
return [self.child_descriptor]
def get_html(self):
if self.child is None:
# raise error instead? In fact, could complain on descriptor load...
return "<div>Nothing to randomize between</div>"
return self.child.get_html()
def get_icon_class(self):
return self.child.get_icon_class() if self.child else 'other'
class RandomizeDescriptor(SequenceDescriptor):
# the editing interface can be the same as for sequences -- just a container
module_class = RandomizeModule
filename_extension = "xml"
stores_state = True
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('randomize')
for child in self.get_children():
xml_object.append(
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object
def has_dynamic_children(self):
"""
Grading needs to know that only one of the children is actually "real". This
makes it use module.get_child_descriptors().
"""
return True
...@@ -26,7 +26,7 @@ test_system = ModuleSystem( ...@@ -26,7 +26,7 @@ test_system = ModuleSystem(
# "render" to just the context... # "render" to just the context...
render_template=lambda template, context: str(context), render_template=lambda template, context: str(context),
replace_urls=Mock(), replace_urls=Mock(),
user=Mock(), user=Mock(is_staff=False),
filestore=Mock(), filestore=Mock(),
debug=True, debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10}, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
......
import datetime
import json
from mock import Mock
from pprint import pprint
import unittest
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
from . import test_system
class CapaFactory(object):
"""
A helper class to create problem modules with various parameters for testing.
"""
sample_problem_xml = """<?xml version="1.0"?>
<problem>
<text>
<p>What is pi, to two decimal placs?</p>
</text>
<numericalresponse answer="3.14">
<textline math="1" size="30"/>
</numericalresponse>
</problem>
"""
num = 0
@staticmethod
def next_num():
CapaFactory.num += 1
return CapaFactory.num
@staticmethod
def create(graceperiod=None,
due=None,
max_attempts=None,
showanswer=None,
rerandomize=None,
force_save_button=None,
attempts=None,
problem_state=None,
):
"""
All parameters are optional, and are added to the created problem if specified.
Arguments:
graceperiod:
due:
max_attempts:
showanswer:
force_save_button:
rerandomize: all strings, as specified in the policy for the problem
problem_state: a dict to to be serialized into the instance_state of the
module.
attempts: also added to instance state. Will be converted to an int.
"""
definition = {'data': CapaFactory.sample_problem_xml,}
location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CapaFactory.next_num())])
metadata = {}
if graceperiod is not None:
metadata['graceperiod'] = graceperiod
if due is not None:
metadata['due'] = due
if max_attempts is not None:
metadata['attempts'] = max_attempts
if showanswer is not None:
metadata['showanswer'] = showanswer
if force_save_button is not None:
metadata['force_save_button'] = force_save_button
if rerandomize is not None:
metadata['rerandomize'] = rerandomize
descriptor = Mock(weight="1")
instance_state_dict = {}
if problem_state is not None:
instance_state_dict = problem_state
if attempts is not None:
# converting to int here because I keep putting "0" and "1" in the tests
# since everything else is a string.
instance_state_dict['attempts'] = int(attempts)
if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict)
else:
instance_state = None
module = CapaModule(test_system, location,
definition, descriptor,
instance_state, None, metadata=metadata)
return module
class CapaModuleTest(unittest.TestCase):
def setUp(self):
now = datetime.datetime.now()
day_delta = datetime.timedelta(days=1)
self.yesterday_str = str(now - day_delta)
self.today_str = str(now)
self.tomorrow_str = str(now + day_delta)
# in the capa grace period format, not in time delta format
self.two_day_delta_str = "2 days"
def test_import(self):
module = CapaFactory.create()
self.assertEqual(module.get_score()['score'], 0)
other_module = CapaFactory.create()
self.assertEqual(module.get_score()['score'], 0)
self.assertNotEqual(module.url_name, other_module.url_name,
"Factory should be creating unique names for each problem")
def test_showanswer_default(self):
"""
Make sure the show answer logic does the right thing.
"""
# default, no due date, showanswer 'closed', so problem is open, and show_answer
# not visible.
problem = CapaFactory.create()
self.assertFalse(problem.answer_available())
def test_showanswer_attempted(self):
problem = CapaFactory.create(showanswer='attempted')
self.assertFalse(problem.answer_available())
problem.attempts = 1
self.assertTrue(problem.answer_available())
def test_showanswer_closed(self):
# can see after attempts used up, even with due date in the future
used_all_attempts = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="1",
due=self.tomorrow_str)
self.assertTrue(used_all_attempts.answer_available())
# can see after due date
after_due_date = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.yesterday_str)
self.assertTrue(after_due_date.answer_available())
# can't see because attempts left
attempts_left_open = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.tomorrow_str)
self.assertFalse(attempts_left_open.answer_available())
# Can't see because grace period hasn't expired
still_in_grace = CapaFactory.create(showanswer='closed',
max_attempts="1",
attempts="0",
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertFalse(still_in_grace.answer_available())
def test_showanswer_past_due(self):
"""
With showanswer="past_due" should only show answer after the problem is closed
for everyone--e.g. after due date + grace period.
"""
# can see after attempts used up, even with due date in the future
used_all_attempts = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="1",
due=self.tomorrow_str)
self.assertFalse(used_all_attempts.answer_available())
# can see after due date
past_due_date = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="0",
due=self.yesterday_str)
self.assertTrue(past_due_date.answer_available())
# can't see because attempts left
attempts_left_open = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="0",
due=self.tomorrow_str)
self.assertFalse(attempts_left_open.answer_available())
# Can't see because grace period hasn't expired, even though have no more
# attempts.
still_in_grace = CapaFactory.create(showanswer='past_due',
max_attempts="1",
attempts="1",
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertFalse(still_in_grace.answer_available())
import json
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 .test_export import DATA_DIR
ORG = 'test_org'
COURSE = 'conditional' # name of directory with course data
from . import test_system
class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
course_id = "/".join([ORG, COURSE, 'test_run'])
course_dir = "test_dir"
policy = {}
error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__(
xmlstore,
course_id,
course_dir,
policy,
error_tracker,
parent_tracker,
load_error_modules=load_error_modules,
)
def render_template(self, template, context):
raise Exception("Shouldn't be called")
class ConditionalModuleTest(unittest.TestCase):
@staticmethod
def get_system(load_error_modules=True):
'''Get a dummy system'''
return DummySystem(load_error_modules)
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
self.modulestore = modulestore
self.assertEquals(len(courses), 1)
return courses[0]
def test_conditional_module(self):
"""Make sure that conditional module works"""
print "Starting import"
course = self.get_course('conditional')
print "Course: ", course
print "id: ", course.id
instance_states = dict(problem=None)
shared_state = None
def inner_get_module(descriptor):
if isinstance(descriptor, Location):
location = descriptor
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location
instance_state = instance_states.get(location.category,None)
print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional","condone"])
module = inner_get_module(location)
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module
print "module: ", module
html = module.get_html()
print "html type: ", type(html)
print "html: ", html
html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-edX-cond_test-conditional-condone', 'id': 'i4x://edX/cond_test/conditional/condone'}"
self.assertEqual(html, html_expect)
gdi = module.get_display_items()
print "gdi=", gdi
ajax = json.loads(module.handle_ajax('',''))
self.assertTrue('xmodule.conditional_module' in ajax['html'])
print "ajax: ", ajax
# now change state of the capa problem to make it completed
instance_states['problem'] = json.dumps({'attempts':1})
ajax = json.loads(module.handle_ajax('',''))
self.assertTrue('This is a secret' in ajax['html'])
print "post-attempt ajax: ", ajax
import unittest
from time import strptime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
ORG = 'test_org'
COURSE = 'test_course'
START = '2013-01-01T01:00:00'
from test_course_module import DummySystem as DummyImportSystem
from . import test_system
class RandomizeModuleTestCase(unittest.TestCase):
"""Make sure the randomize module works"""
@staticmethod
def get_dummy_course(start):
"""Get a dummy course"""
system = DummyImportSystem(load_error_modules=True)
def to_attrb(n, v):
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
start_xml = '''
<course org="{org}" course="{course}"
graceperiod="1 day" url_name="test"
start="{start}"
>
<chapter url="hi" url_name="ch" display_name="CH">
<randomize url_name="my_randomize">
<html url_name="a" display_name="A">Two houses, ...</html>
<html url_name="b" display_name="B">Three houses, ...</html>
</randomize>
</chapter>
</course>
'''.format(org=ORG, course=COURSE, start=start)
return system.process_xml(start_xml)
def test_import(self):
"""
Just make sure descriptor loads without error
"""
descriptor = self.get_dummy_course(START)
# TODO: add tests that create a module and check. Passing state is a good way to
# check that child access works...
...@@ -48,3 +48,5 @@ class VerticalDescriptor(SequenceDescriptor): ...@@ -48,3 +48,5 @@ class VerticalDescriptor(SequenceDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor" js_module_name = "VerticalDescriptor"
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
# like verticals will get exported as sequentials...
...@@ -585,6 +585,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -585,6 +585,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._inherited_metadata.add(attr) self._inherited_metadata.add(attr)
self.metadata[attr] = metadata[attr] self.metadata[attr] = metadata[attr]
def get_required_module_descriptors(self):
"""Returns a list of XModuleDescritpor instances upon which this module depends, but are
not children of this module"""
return []
def get_children(self): def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of """Returns a list of XModuleDescriptor instances for the children of
this module""" this module"""
......
course for testing conditional module
<conditional condition="require_attempted" required="problem/choiceprob">
<html url_name="secret_page" />
</conditional>
<course name="Conditional Course" org="edX" course="cond_test" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
<chapter name="Problems with Condition">
<sequential>
<problem url_name="choiceprob" />
<conditional url_name="condone"/>
</sequential>
</chapter>
</course>
<html display_name="Secret Page">
<p>This is a secret!</p>
</html>
<problem display_name="S3E2: Lorentz Force">
<startouttext/>
<p>Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to left in the plane of your screen. A diagram of this situation is show below…</p>
<center><img width="400" src="/static/images/LSQimages/LSQ_W01_8.png"/></center>
<p>a. The magnitude of the force experienced by the electron is proportional the product of which of the following? (Select all that apply.)</p>
<endouttext/>
<choiceresponse>
<checkboxgroup>
<!-- include ellipses to test non-ascii characters -->
<choice correct="true"><text>Magnetic field strength…</text></choice>
<choice correct="false"><text>Electric field strength…</text></choice>
<choice correct="true"><text>Electric charge of the electron…</text></choice>
<choice correct="false"><text>Radius of the electron…</text></choice>
<choice correct="false"><text>Mass of the electron…</text></choice>
<choice correct="true"><text>Velocity of the electron…</text></choice>
</checkboxgroup>
</choiceresponse>
</problem>
...@@ -141,6 +141,7 @@ That's basically all there is to the organizational structure. Read the next se ...@@ -141,6 +141,7 @@ That's basically all there is to the organizational structure. Read the next se
* `abtest` -- Support for A/B testing. TODO: add details.. * `abtest` -- Support for A/B testing. TODO: add details..
* `chapter` -- top level organization unit of a course. The courseware display code currently expects the top level `course` element to contain only chapters, though there is no philosophical reason why this is required, so we may change it to properly display non-chapters at the top level. * `chapter` -- top level organization unit of a course. The courseware display code currently expects the top level `course` element to contain only chapters, though there is no philosophical reason why this is required, so we may change it to properly display non-chapters at the top level.
* `conditional` -- conditional element, which shows one or more modules only if certain conditions are satisfied.
* `course` -- top level tag. Contains everything else. * `course` -- top level tag. Contains everything else.
* `customtag` -- render an html template, filling in some parameters, and return the resulting html. See below for details. * `customtag` -- render an html template, filling in some parameters, and return the resulting html. See below for details.
* `discussion` -- Inline discussion forum * `discussion` -- Inline discussion forum
...@@ -163,6 +164,22 @@ Container tags include `chapter`, `sequential`, `videosequence`, `vertical`, and ...@@ -163,6 +164,22 @@ Container tags include `chapter`, `sequential`, `videosequence`, `vertical`, and
`course` is also a container, and is similar, with one extra wrinkle: the top level pointer tag _must_ have `org` and `course` attributes specified--the organization name, and course name. Note that `course` is referring to the platonic ideal of this course (e.g. "6.002x"), not to any particular run of this course. The `url_name` should be the particular run of this course. `course` is also a container, and is similar, with one extra wrinkle: the top level pointer tag _must_ have `org` and `course` attributes specified--the organization name, and course name. Note that `course` is referring to the platonic ideal of this course (e.g. "6.002x"), not to any particular run of this course. The `url_name` should be the particular run of this course.
### `conditional`
`conditional` is as special kind of container tag as well. Here are two examples:
<conditional condition="require_completed" required="problem/choiceprob">
<video url_name="secret_video" />
</conditional>
<conditional condition="require_attempted" required="problem/choiceprob&problem/sumprob">
<html url_name="secret_page" />
</conditional>
The condition can be either `require_completed`, in which case the required modules must be completed, or `require_attempted`, in which case the required modules must have been attempted.
The required modules are specified as a set of `tag`/`url_name`, joined by an ampersand.
### `customtag` ### `customtag`
When we see `<customtag impl="special" animal="unicorn" hat="blue"/>`, we will: When we see `<customtag impl="special" animal="unicorn" hat="blue"/>`, we will:
......
...@@ -113,6 +113,9 @@ class StudentModuleCache(object): ...@@ -113,6 +113,9 @@ class StudentModuleCache(object):
descriptor_filter=lambda descriptor: True, descriptor_filter=lambda descriptor: True,
select_for_update=False): select_for_update=False):
""" """
obtain and return cache for descriptor descendents (ie children) AND modules required by the descriptor,
but which are not children of the module
course_id: the course in the context of which we want StudentModules. course_id: the course in the context of which we want StudentModules.
user: the django user for whom to load modules. user: the django user for whom to load modules.
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
...@@ -132,7 +135,7 @@ class StudentModuleCache(object): ...@@ -132,7 +135,7 @@ class StudentModuleCache(object):
if depth is None or depth > 0: if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children(): for child in descriptor.get_children() + descriptor.get_required_module_descriptors():
descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter)) descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter))
return descriptors return descriptors
......
...@@ -21,6 +21,8 @@ class ControllerQueryService(GradingService): ...@@ -21,6 +21,8 @@ class ControllerQueryService(GradingService):
self.is_unique_url = self.url + '/is_name_unique/' self.is_unique_url = self.url + '/is_name_unique/'
self.combined_notifications_url = self.url + '/combined_notifications/' self.combined_notifications_url = self.url + '/combined_notifications/'
self.grading_status_list_url = self.url + '/get_grading_status_list/' self.grading_status_list_url = self.url + '/get_grading_status_list/'
self.flagged_problem_list_url = self.url + '/get_flagged_problem_list/'
self.take_action_on_flags_url = self.url + '/take_action_on_flags/'
def check_if_name_is_unique(self, location, problem_id, course_id): def check_if_name_is_unique(self, location, problem_id, course_id):
params = { params = {
...@@ -57,3 +59,23 @@ class ControllerQueryService(GradingService): ...@@ -57,3 +59,23 @@ class ControllerQueryService(GradingService):
response = self.get(self.grading_status_list_url, params) response = self.get(self.grading_status_list_url, params)
return response return response
def get_flagged_problem_list(self, course_id):
params = {
'course_id' : course_id,
}
response = self.get(self.flagged_problem_list_url, params)
return response
def take_action_on_flags(self, course_id, student_id, submission_id, action_type):
params = {
'course_id' : course_id,
'student_id' : student_id,
'submission_id' : submission_id,
'action_type' : action_type
}
response = self.post(self.take_action_on_flags_url, params)
return response
...@@ -19,7 +19,8 @@ KEY_PREFIX = "open_ended_" ...@@ -19,7 +19,8 @@ KEY_PREFIX = "open_ended_"
NOTIFICATION_TYPES = ( NOTIFICATION_TYPES = (
('student_needs_to_peer_grade', 'peer_grading', 'Peer Grading'), ('student_needs_to_peer_grade', 'peer_grading', 'Peer Grading'),
('staff_needs_to_grade', 'staff_grading', 'Staff Grading'), ('staff_needs_to_grade', 'staff_grading', 'Staff Grading'),
('new_student_grading_to_view', 'open_ended_problems', 'Problems you have submitted') ('new_student_grading_to_view', 'open_ended_problems', 'Problems you have submitted'),
('flagged_submissions_exist', 'open_ended_flagged_problems', 'Flagged Submissions')
) )
def staff_grading_notifications(course, user): def staff_grading_notifications(course, user):
......
...@@ -50,7 +50,7 @@ class MockPeerGradingService(object): ...@@ -50,7 +50,7 @@ class MockPeerGradingService(object):
'max_score': 4}) 'max_score': 4})
def save_grade(self, location, grader_id, submission_id, def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores): score, feedback, submission_key, rubric_scores, submission_flagged):
return json.dumps({'success': True}) return json.dumps({'success': True})
def is_student_calibrated(self, problem_location, grader_id): def is_student_calibrated(self, problem_location, grader_id):
...@@ -97,7 +97,7 @@ class PeerGradingService(GradingService): ...@@ -97,7 +97,7 @@ class PeerGradingService(GradingService):
{'location': problem_location, 'grader_id': grader_id}) {'location': problem_location, 'grader_id': grader_id})
return json.dumps(self._render_rubric(response)) return json.dumps(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores): def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
data = {'grader_id' : grader_id, data = {'grader_id' : grader_id,
'submission_id' : submission_id, 'submission_id' : submission_id,
'score' : score, 'score' : score,
...@@ -105,7 +105,8 @@ class PeerGradingService(GradingService): ...@@ -105,7 +105,8 @@ class PeerGradingService(GradingService):
'submission_key': submission_key, 'submission_key': submission_key,
'location': location, 'location': location,
'rubric_scores': rubric_scores, 'rubric_scores': rubric_scores,
'rubric_scores_complete': True} 'rubric_scores_complete': True,
'submission_flagged' : submission_flagged}
return self.post(self.save_grade_url, data) return self.post(self.save_grade_url, data)
def is_student_calibrated(self, problem_location, grader_id): def is_student_calibrated(self, problem_location, grader_id):
...@@ -233,7 +234,7 @@ def save_grade(request, course_id): ...@@ -233,7 +234,7 @@ def save_grade(request, course_id):
error: if there was an error in the submission, this is the error message error: if there was an error in the submission, this is the error message
""" """
_check_post(request) _check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]']) required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged'])
success, message = _check_required(request, required) success, message = _check_required(request, required)
if not success: if not success:
return _err_response(message) return _err_response(message)
...@@ -245,9 +246,10 @@ def save_grade(request, course_id): ...@@ -245,9 +246,10 @@ def save_grade(request, course_id):
feedback = p['feedback'] feedback = p['feedback']
submission_key = p['submission_key'] submission_key = p['submission_key']
rubric_scores = p.getlist('rubric_scores[]') rubric_scores = p.getlist('rubric_scores[]')
submission_flagged = p['submission_flagged']
try: try:
response = peer_grading_service().save_grade(location, grader_id, submission_id, response = peer_grading_service().save_grade(location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores) score, feedback, submission_key, rubric_scores, submission_flagged)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2}, log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
......
...@@ -172,7 +172,8 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -172,7 +172,8 @@ class TestPeerGradingService(ct.PageLoader):
'submission_key': 'fake key', 'submission_key': 'fake key',
'score': '2', 'score': '2',
'feedback': 'This is feedback', 'feedback': 'This is feedback',
'rubric_scores[]': [1, 2]} 'rubric_scores[]': [1, 2],
'submission_flagged' : False}
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success']) self.assertTrue(d['success'])
......
...@@ -25,6 +25,8 @@ import open_ended_notifications ...@@ -25,6 +25,8 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search from xmodule.modulestore import search
from django.http import HttpResponse, Http404
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
...@@ -54,12 +56,14 @@ def _reverse_without_slash(url_name, course_id): ...@@ -54,12 +56,14 @@ def _reverse_without_slash(url_name, course_id):
DESCRIPTION_DICT = { DESCRIPTION_DICT = {
'Peer Grading': "View all problems that require peer assessment in this particular course.", 'Peer Grading': "View all problems that require peer assessment in this particular course.",
'Staff Grading': "View ungraded submissions submitted by students for the open ended problems in the course.", 'Staff Grading': "View ungraded submissions submitted by students for the open ended problems in the course.",
'Problems you have submitted': "View open ended problems that you have previously submitted for grading." 'Problems you have submitted': "View open ended problems that you have previously submitted for grading.",
'Flagged Submissions' : "View submissions that have been flagged by students as inappropriate."
} }
ALERT_DICT = { ALERT_DICT = {
'Peer Grading': "New submissions to grade", 'Peer Grading': "New submissions to grade",
'Staff Grading': "New submissions to grade", 'Staff Grading': "New submissions to grade",
'Problems you have submitted': "New grades have been returned" 'Problems you have submitted': "New grades have been returned",
'Flagged Submissions' : "Submissions have been flagged for review"
} }
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def staff_grading(request, course_id): def staff_grading(request, course_id):
...@@ -158,7 +162,8 @@ def student_problem_list(request, course_id): ...@@ -158,7 +162,8 @@ def student_problem_list(request, course_id):
success = problem_list_dict['success'] success = problem_list_dict['success']
if 'error' in problem_list_dict: if 'error' in problem_list_dict:
error_text = problem_list_dict['error'] error_text = problem_list_dict['error']
problem_list = []
else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
for i in xrange(0,len(problem_list)): for i in xrange(0,len(problem_list)):
...@@ -194,11 +199,57 @@ def student_problem_list(request, course_id): ...@@ -194,11 +199,57 @@ def student_problem_list(request, course_id):
'staff_access': False, }) 'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def flagged_problem_list(request, course_id):
'''
Show a student problem list
'''
course = get_course_with_access(request.user, course_id, 'staff')
student_id = unique_id_for_user(request.user)
# call problem list service
success = False
error_text = ""
problem_list = []
base_course_url = reverse('courses')
try:
problem_list_json = controller_qs.get_flagged_problem_list(course_id)
problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list=[]
else:
problem_list = problem_list_dict['flagged_submissions']
except GradingServiceError:
error_text = "Error occured while contacting the grading service"
success = False
# catch error if if the json loads fails
except ValueError:
error_text = "Could not get problem list"
success = False
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', {
'course': course,
'course_id': course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': True, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def combined_notifications(request, course_id): def combined_notifications(request, course_id):
"""
Gets combined notifications from the grading controller and displays them
"""
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
user = request.user user = request.user
notifications = open_ended_notifications.combined_notifications(course, user) notifications = open_ended_notifications.combined_notifications(course, user)
log.debug(notifications)
response = notifications['response'] response = notifications['response']
notification_tuples=open_ended_notifications.NOTIFICATION_TYPES notification_tuples=open_ended_notifications.NOTIFICATION_TYPES
...@@ -244,4 +295,34 @@ def combined_notifications(request, course_id): ...@@ -244,4 +295,34 @@ def combined_notifications(request, course_id):
combined_dict combined_dict
) )
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def take_action_on_flags(request, course_id):
"""
Takes action on student flagged submissions.
Currently, only support unflag and ban actions.
"""
if request.method != 'POST':
raise Http404
required = ['submission_id', 'action_type', 'student_id']
for key in required:
if key not in request.POST:
return HttpResponse(json.dumps({'success': False, 'error': 'Missing key {0}'.format(key)}),
mimetype="application/json")
p = request.POST
submission_id = p['submission_id']
action_type = p['action_type']
student_id = p['student_id']
student_id = student_id.strip(' \t\n\r')
submission_id = submission_id.strip(' \t\n\r')
action_type = action_type.lower().strip(' \t\n\r')
try:
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
return _err_response('Could not connect to grading service')
...@@ -438,6 +438,7 @@ main_vendor_js = [ ...@@ -438,6 +438,7 @@ main_vendor_js = [
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee')) peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/open_ended/**/*.coffee'))
PIPELINE_CSS = { PIPELINE_CSS = {
'application': { 'application': {
...@@ -468,7 +469,7 @@ PIPELINE_JS = { ...@@ -468,7 +469,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js) set(courseware_js + discussion_js + staff_grading_js + peer_grading_js + open_ended_js)
) + [ ) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
...@@ -501,6 +502,10 @@ PIPELINE_JS = { ...@@ -501,6 +502,10 @@ PIPELINE_JS = {
'peer_grading' : { 'peer_grading' : {
'source_filenames': peer_grading_js, 'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js' 'output_filename': 'js/peer_grading.js'
},
'open_ended' : {
'source_filenames': open_ended_js,
'output_filename': 'js/open_ended.js'
} }
} }
...@@ -530,7 +535,7 @@ PIPELINE_COMPILERS = [ ...@@ -530,7 +535,7 @@ PIPELINE_COMPILERS = [
'pipeline.compilers.coffee.CoffeeScriptCompiler', 'pipeline.compilers.coffee.CoffeeScriptCompiler',
] ]
PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_SASS_ARGUMENTS = '-t expanded -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
PIPELINE_CSS_COMPRESSOR = None PIPELINE_CSS_COMPRESSOR = None
PIPELINE_JS_COMPRESSOR = None PIPELINE_JS_COMPRESSOR = None
...@@ -540,7 +545,7 @@ STATICFILES_IGNORE_PATTERNS = ( ...@@ -540,7 +545,7 @@ STATICFILES_IGNORE_PATTERNS = (
"coffee/*", "coffee/*",
) )
PIPELINE_YUI_BINARY = 'yui-compressor' # PIPELINE_YUI_BINARY = 'yui-compressor'
PIPELINE_SASS_BINARY = 'sass' PIPELINE_SASS_BINARY = 'sass'
PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee' PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee'
......
# This is a simple class that just hides the error container
# and message container when they are empty
# Can (and should be) expanded upon when our problem list
# becomes more sophisticated
class OpenEnded
constructor: (ajax_url) ->
@ajax_url = ajax_url
@error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_list = $('.problem-list')
@ban_button = $('.ban-button')
@unflag_button = $('.unflag-button')
@ban_button.click @ban
@unflag_button.click @unflag
unflag: (event) =>
event.preventDefault()
parent_tr = $(event.target).parent().parent()
tr_children = parent_tr.children()
action_type = "unflag"
submission_id = parent_tr.data('submission-id')
student_id = parent_tr.data('student-id')
callback_func = @after_action_wrapper($(event.target), action_type)
@post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, callback_func)
ban: (event) =>
event.preventDefault()
parent_tr = $(event.target).parent().parent()
tr_children = parent_tr.children()
action_type = "ban"
submission_id = parent_tr.data('submission-id')
student_id = parent_tr.data('student-id')
callback_func = @after_action_wrapper($(event.target), action_type)
@post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, callback_func)
post: (cmd, data, callback) ->
# if this post request fails, the error callback will catch it
$.post(@ajax_url + cmd, data, callback)
.error => callback({success: false, error: "Error occured while performing this operation"})
after_action_wrapper: (target, action_type) ->
tr_parent = target.parent().parent()
tr_children = tr_parent.children()
action_taken = tr_children[4].firstElementChild
action_taken.innerText = "#{action_type} done for student."
return @handle_after_action
handle_after_action: (data) ->
if !data.success
@gentle_alert data.error
gentle_alert: (msg) =>
if $('.message-container').length
$('.message-container').remove()
alert_elem = "<div class='message-container'>" + msg + "</div>"
$('.error-container').after(alert_elem)
$('.message-container').css(opacity: 0).animate(opacity: 1, 700)
ajax_url = $('.open-ended-problems').data('ajax_url')
$(document).ready(() -> new OpenEnded(ajax_url))
...@@ -175,6 +175,7 @@ class PeerGradingProblem ...@@ -175,6 +175,7 @@ class PeerGradingProblem
@submission_container = $('.submission-container') @submission_container = $('.submission-container')
@prompt_container = $('.prompt-container') @prompt_container = $('.prompt-container')
@rubric_container = $('.rubric-container') @rubric_container = $('.rubric-container')
@flag_student_container = $('.flag-student-container')
@calibration_panel = $('.calibration-panel') @calibration_panel = $('.calibration-panel')
@grading_panel = $('.grading-panel') @grading_panel = $('.grading-panel')
@content_panel = $('.content-panel') @content_panel = $('.content-panel')
...@@ -201,6 +202,7 @@ class PeerGradingProblem ...@@ -201,6 +202,7 @@ class PeerGradingProblem
@action_button = $('.action-button') @action_button = $('.action-button')
@calibration_feedback_button = $('.calibration-feedback-button') @calibration_feedback_button = $('.calibration-feedback-button')
@interstitial_page_button = $('.interstitial-page-button') @interstitial_page_button = $('.interstitial-page-button')
@flag_student_checkbox = $('.flag-checkbox')
Collapsible.setCollapsibles(@content_panel) Collapsible.setCollapsibles(@content_panel)
...@@ -241,6 +243,7 @@ class PeerGradingProblem ...@@ -241,6 +243,7 @@ class PeerGradingProblem
submission_id: @essay_id_input.val() submission_id: @essay_id_input.val()
submission_key: @submission_key_input.val() submission_key: @submission_key_input.val()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_flagged: @flag_student_checkbox.is(':checked')
return data return data
...@@ -333,7 +336,7 @@ class PeerGradingProblem ...@@ -333,7 +336,7 @@ class PeerGradingProblem
@grading_panel.find('.calibration-text').show() @grading_panel.find('.calibration-text').show()
@calibration_panel.find('.grading-text').hide() @calibration_panel.find('.grading-text').hide()
@grading_panel.find('.grading-text').hide() @grading_panel.find('.grading-text').hide()
@flag_student_container.hide()
@submit_button.unbind('click') @submit_button.unbind('click')
@submit_button.click @submit_calibration_essay @submit_button.click @submit_calibration_essay
...@@ -360,6 +363,7 @@ class PeerGradingProblem ...@@ -360,6 +363,7 @@ class PeerGradingProblem
@grading_panel.find('.calibration-text').hide() @grading_panel.find('.calibration-text').hide()
@calibration_panel.find('.grading-text').show() @calibration_panel.find('.grading-text').show()
@grading_panel.find('.grading-text').show() @grading_panel.find('.grading-text').show()
@flag_student_container.show()
@submit_button.unbind('click') @submit_button.unbind('click')
@submit_button.click @submit_grade @submit_button.click @submit_grade
......
<div id="conditional_${element_id}" class="conditional-wrapper" data-problem-id="${id}" data-url="${ajax_url}"></div>
<%
from django.core.urlresolvers import reverse
reqm = module.required_modules[0]
course_id = module.system.course_id
condition = module.condition
%>
<p><a href="${reverse('jump_to',kwargs=dict(course_id=course_id, location=reqm.location.url()))}">${reqm.display_name}</a>
must be
% if 'attempted' in condition:
attempted
% else:
completed
% endif
before this will become visible.</p>
<%inherit file="/main.html" />
<%block name="bodyclass">${course.css_class}</%block>
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} Flagged Open Ended Problems</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" />
<%block name="js_extra">
<%static:js group='open_ended'/>
</%block>
<section class="container">
<div class="open-ended-problems" data-ajax_url="${ajax_url}">
<div class="error-container">${error_text}</div>
<h1>Flagged Open Ended Problems</h1>
<h2>Instructions</h2>
<p>Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.</p>
% if success:
% if len(problem_list) == 0:
<div class="message-container">
No flagged problems exist.
</div>
%else:
<table class="problem-list">
<tr>
<th>Name</th>
<th>Response</th>
<th></th>
<th></th>
</tr>
%for problem in problem_list:
<tr data-submission-id="${problem['submission_id']}" data-student-id="${problem['student_id']}">
<td>
${problem['problem_name']}
</td>
<td>
${problem['student_response']}
</td>
<td>
<a href="#unflag" class="unflag-button action-button" data-action-type="unflag">Unflag</a>
</td>
<td>
<a href="#ban" class="ban-button action-button" data-action-type="ban">Ban</a>
</td>
<td>
<div class="action-taken"></div>
</td>
</tr>
%endfor
</table>
%endif
%endif
</div>
</section>
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
<p>Please include some written feedback as well.</p> <p>Please include some written feedback as well.</p>
<textarea name="feedback" placeholder="Feedback for student" <textarea name="feedback" placeholder="Feedback for student"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
<p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p>
</div> </div>
......
...@@ -289,6 +289,12 @@ if settings.COURSEWARE_ENABLED: ...@@ -289,6 +289,12 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
'open_ended_grading.views.student_problem_list', name='open_ended_problems'), 'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
# Open Ended flagged problem list
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems/take_action_on_flags$',
'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
# Cohorts management # Cohorts management
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts$',
'course_groups.views.list_cohorts', name="cohorts"), 'course_groups.views.list_cohorts', name="cohorts"),
......
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