Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/export-draft-modules
This commit is contained in:
@@ -34,6 +34,7 @@ MITX_FEATURES = {
|
||||
'ENABLE_DISCUSSION_SERVICE': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES': False,
|
||||
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
|
||||
'STUDIO_NPS_SURVEY': True,
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
|
||||
@@ -147,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
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
<%include file="widgets/tender.html" />
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
<%include file="widgets/qualaroo.html" />
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
13
cms/templates/widgets/qualaroo.html
Normal file
13
cms/templates/widgets/qualaroo.html
Normal file
@@ -0,0 +1,13 @@
|
||||
% 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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}']")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
'''
|
||||
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)
|
||||
|
||||
@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))
|
||||
Reference in New Issue
Block a user