Commit b61d7b2c by Chris Dodge

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

parents 1d8f9c22 7c987587
...@@ -34,6 +34,7 @@ MITX_FEATURES = { ...@@ -34,6 +34,7 @@ MITX_FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STUDIO_NPS_SURVEY': True,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -147,3 +147,6 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -147,3 +147,6 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = True DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
...@@ -58,6 +58,8 @@ ...@@ -58,6 +58,8 @@
<%include file="widgets/tender.html" /> <%include file="widgets/tender.html" />
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
<%include file="widgets/qualaroo.html" />
</html> </html>
% 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): ...@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')], resource_string(__name__, 'js/src/annotatable/display.coffee')],
'js': [] 'js': []}
}
js_module_name = "Annotatable" js_module_name = "Annotatable"
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable' icon_class = 'annotatable'
...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule):
if color is not None: if color is not None:
if color in self.highlight_colors: if color in self.highlight_colors:
cls.append('highlight-'+color) cls.append('highlight-' + color)
attr['_delete'] = highlight_key attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls) attr['value'] = ' '.join(cls)
return { 'class' : attr } return {'class': attr}
def _get_annotation_data_attr(self, index, el): def _get_annotation_data_attr(self, index, el):
""" Returns a dict in which the keys are the HTML data attributes """ Returns a dict in which the keys are the HTML data attributes
...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
if xml_key in el.attrib: if xml_key in el.attrib:
value = el.get(xml_key, '') value = el.get(xml_key, '')
html_key = attrs_map[xml_key] html_key = attrs_map[xml_key]
data_attrs[html_key] = { 'value': value, '_delete': xml_key } data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs return data_attrs
...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
delete_key = attr[key]['_delete'] delete_key = attr[key]['_delete']
del el.attrib[delete_key] del el.attrib[delete_key]
def _render_content(self): def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """ """ Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content) xmltree = etree.fromstring(self.content)
...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor): ...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
stores_state = True stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
/* TODO: move top-level variables to a common _variables.scss.
* NOTE: These variables were only added here because when this was integrated with the CMS,
* SASS compilation errors were triggered because the CMS didn't have the same variables defined
* that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
* -Abarrett and Vshnayder
*/
$border-color: #C8C8C8; $border-color: #C8C8C8;
$body-font-size: em(14); $body-font-size: em(14);
.annotatable-wrapper {
position: relative;
}
.annotatable-header { .annotatable-header {
margin-bottom: .5em; margin-bottom: .5em;
.annotatable-title { .annotatable-title {
...@@ -55,6 +65,7 @@ $body-font-size: em(14); ...@@ -55,6 +65,7 @@ $body-font-size: em(14);
display: inline; display: inline;
cursor: pointer; cursor: pointer;
$highlight_index: 0;
@each $highlight in ( @each $highlight in (
(yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)), (yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)),
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)), (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
...@@ -62,12 +73,13 @@ $body-font-size: em(14); ...@@ -62,12 +73,13 @@ $body-font-size: em(14);
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)), (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)), (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) { (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
$highlight_index: $highlight_index + 1;
$marker: nth($highlight,1); $marker: nth($highlight,1);
$color: nth($highlight,2); $color: nth($highlight,2);
$selected_color: nth($highlight,3); $selected_color: nth($highlight,3);
@if $marker == yellow { @if $highlight_index == 1 {
&.highlight { &.highlight {
background-color: $color; background-color: $color;
&.selected { background-color: $selected_color; } &.selected { background-color: $selected_color; }
...@@ -127,6 +139,7 @@ $body-font-size: em(14); ...@@ -127,6 +139,7 @@ $body-font-size: em(14);
font-weight: 400; font-weight: 400;
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
background-color: transparent; background-color: transparent;
border-color: transparent;
} }
p { p {
color: inherit; color: inherit;
...@@ -143,6 +156,7 @@ $body-font-size: em(14); ...@@ -143,6 +156,7 @@ $body-font-size: em(14);
margin: 0px 0px 10px 0; margin: 0px 0px 10px 0;
max-height: 225px; max-height: 225px;
overflow: auto; overflow: auto;
line-height: normal;
} }
.annotatable-reply { .annotatable-reply {
display: block; display: block;
...@@ -165,5 +179,3 @@ $body-font-size: em(14); ...@@ -165,5 +179,3 @@ $body-font-size: em(14);
border-top-color: rgba(0, 0, 0, .85); border-top-color: rgba(0, 0, 0, .85);
} }
} }
class @Annotatable class @Annotatable
_debug: false _debug: false
# selectors for the annotatable xmodule # selectors for the annotatable xmodule
wrapperSelector: '.annotatable-wrapper'
toggleAnnotationsSelector: '.annotatable-toggle-annotations' toggleAnnotationsSelector: '.annotatable-toggle-annotations'
toggleInstructionsSelector: '.annotatable-toggle-instructions' toggleInstructionsSelector: '.annotatable-toggle-instructions'
instructionsSelector: '.annotatable-instructions' instructionsSelector: '.annotatable-instructions'
...@@ -61,7 +62,7 @@ class @Annotatable ...@@ -61,7 +62,7 @@ class @Annotatable
my: 'bottom center' # of tooltip my: 'bottom center' # of tooltip
at: 'top center' # of target at: 'top center' # of target
target: $(el) # where the tooltip was triggered (i.e. the annotation span) target: $(el) # where the tooltip was triggered (i.e. the annotation span)
container: @$el container: @$(@wrapperSelector)
adjust: adjust:
y: -5 y: -5
show: show:
...@@ -75,6 +76,7 @@ class @Annotatable ...@@ -75,6 +76,7 @@ class @Annotatable
classes: 'ui-tooltip-annotatable' classes: 'ui-tooltip-annotatable'
events: events:
show: @onShowTip show: @onShowTip
move: @onMoveTip
onClickToggleAnnotations: (e) => @toggleAnnotations() onClickToggleAnnotations: (e) => @toggleAnnotations()
...@@ -87,6 +89,55 @@ class @Annotatable ...@@ -87,6 +89,55 @@ class @Annotatable
onShowTip: (event, api) => onShowTip: (event, api) =>
event.preventDefault() if @annotationsHidden event.preventDefault() if @annotationsHidden
onMoveTip: (event, api, position) =>
###
This method handles an edge case in which a tooltip is displayed above
a non-overlapping span like this:
(( TOOLTIP ))
\/
text text text ... text text text ...... <span span span>
<span span span>
The problem is that the tooltip looks disconnected from both spans, so
we should re-position the tooltip to appear above the span.
###
tip = api.elements.tooltip
adjust_y = api.options.position?.adjust?.y || 0
container = api.options.position?.container || $('body')
target = api.elements.target
rects = $(target).get(0).getClientRects()
is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right)
if is_non_overlapping
# we want to choose the largest of the two non-overlapping spans and display
# the tooltip above the center of it (see api.options.position settings)
focus_rect = (if rects[0].width > rects[1].width then rects[0] else rects[1])
rect_center = focus_rect.left + (focus_rect.width / 2)
rect_top = focus_rect.top
tip_width = $(tip).width()
tip_height = $(tip).height()
# tooltip is positioned relative to its container, so we need to factor in offsets
container_offset = $(container).offset()
offset_left = -container_offset.left
offset_top = $(document).scrollTop() - container_offset.top
tip_left = offset_left + rect_center - (tip_width / 2)
tip_top = offset_top + rect_top - tip_height + adjust_y
# make sure the new tip position doesn't clip the edges of the screen
win_width = $(window).width()
if tip_left < offset_left
tip_left = offset_left
else if tip_left + tip_width > win_width + offset_left
tip_left = win_width + offset_left - tip_width
# final step: update the position object (used by qtip2 to show the tip after the move event)
$.extend position, 'left': tip_left, 'top': tip_top
getSpanForProblemReturn: (el) -> getSpanForProblemReturn: (el) ->
problem_id = $(@problemReturnSelector).index(el) problem_id = $(@problemReturnSelector).index(el)
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']") @$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
......
...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
else: else:
# VS[compat] # VS[compat]
# cdodge: filesystem static content support. # cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(getattr(self, 'data_dir', None)) caption_asset_path = "/static/subs/"
return self.system.render_template('videoalpha.html', { return self.system.render_template('videoalpha.html', {
'youtube_streams': self.youtube_streams, 'youtube_streams': self.youtube_streams,
......
'''
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))
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