@@ -38,10 +38,10 @@
${problem['student_response']}
|
-
+ Unflag
|
-
+ Ban
|
${problem['submission_id']}
diff --git a/lms/urls.py b/lms/urls.py
index f122635821..41e8e9fff1 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -287,6 +287,8 @@ if settings.COURSEWARE_ENABLED:
'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
# Open Ended flagged problem list
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
+ 'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
From 4c164795691c126ef3d08d5be0e7cba04cfabd0d Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:22:59 -0500
Subject: [PATCH 09/54] Working on frontend JS for posting
---
lms/djangoapps/open_ended_grading/views.py | 2 +-
.../coffee/src/open_ended/open_ended.coffee | 33 +++++++++++++++++++
.../open_ended_flagged_problems.html | 8 +++--
3 files changed, 40 insertions(+), 3 deletions(-)
create mode 100644 lms/static/coffee/src/open_ended/open_ended.coffee
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
index aaee9f4c98..137cff7803 100644
--- a/lms/djangoapps/open_ended_grading/views.py
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -309,7 +309,7 @@ def take_action_on_flags(request, course_id):
try:
controller_qs = ControllerQueryService()
- response = controller_qs.take_action_on_flags(course_id, student_id, course_id, action_type)
+ 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))
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
new file mode 100644
index 0000000000..f45efeb8a7
--- /dev/null
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -0,0 +1,33 @@
+# 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()
+
+ ban: (event) =>
+ event.preventDefault()
+
+ 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"})
+
+ajax_url = $('.open-ended-problems').data('ajax_url')
+$(document).ready(() -> new OpenEnded(ajax_url))
diff --git a/lms/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
index 2397e1a70e..9265ad4663 100644
--- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html
+++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
@@ -10,6 +10,10 @@
<%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" />
+<%block name="js_extra">
+ <%static:js group='open_ended'/>
+%block>
+
${error_text}
@@ -38,10 +42,10 @@
${problem['student_response']}
|
- Unflag
+ Unflag
|
- Ban
+ Ban
|
${problem['submission_id']}
From 10c7155d4d061d51eee8572a4a5b6fd5f1da03eb Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:28:42 -0500
Subject: [PATCH 10/54] Add open ended to JS pipeline
---
lms/envs/common.py | 7 ++++++-
lms/static/coffee/src/open_ended/open_ended.coffee | 9 +++++++++
lms/urls.py | 4 ++--
3 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 16472795e0..426c29c7d0 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -438,6 +438,7 @@ main_vendor_js = [
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'))
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 = {
'application': {
@@ -468,7 +469,7 @@ PIPELINE_JS = {
'source_filenames': sorted(
set(rooted_glob(COMMON_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/my_courses_dropdown.js',
@@ -501,6 +502,10 @@ PIPELINE_JS = {
'peer_grading' : {
'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js'
+ },
+ 'open_ended' : {
+ 'source_filenames': open_ended_js,
+ 'output_filename': 'js/open_ended.js'
}
}
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index f45efeb8a7..5c0f455ce7 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -20,14 +20,23 @@ class OpenEnded
unflag: (event) =>
event.preventDefault()
+ @gentle_alert "Unflag"
ban: (event) =>
event.preventDefault()
+ @gentle_alert "Ban"
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"})
+ gentle_alert: (msg) =>
+ if $('.message-container').length
+ $('.message-container').remove()
+ alert_elem = " " + msg + " "
+ $('.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))
diff --git a/lms/urls.py b/lms/urls.py
index 41e8e9fff1..260f55dd05 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -289,8 +289,8 @@ if settings.COURSEWARE_ENABLED:
# Open Ended flagged problem list
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
- 'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems/take_action_on_flag$',
+ 'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
# Cohorts management
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts$',
From 8e9ec501a777a374491db1c48b5cd5a8c461df24 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:44:21 -0500
Subject: [PATCH 11/54] Implement flagging, fix urls
---
lms/djangoapps/open_ended_grading/views.py | 3 +--
lms/static/coffee/src/open_ended/open_ended.coffee | 11 ++++++++++-
lms/urls.py | 2 +-
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
index 137cff7803..a0ef8239f3 100644
--- a/lms/djangoapps/open_ended_grading/views.py
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -226,7 +226,7 @@ def flagged_problem_list(request, course_id):
error_text = "Could not get problem list"
success = False
- ajax_url = _reverse_with_slash('open_ended_problems', course_id)
+ 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,
@@ -308,7 +308,6 @@ def take_action_on_flags(request, course_id):
student_id = p['student_id']
try:
- controller_qs = ControllerQueryService()
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index 5c0f455ce7..07b84c8af5 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -24,13 +24,22 @@ class OpenEnded
ban: (event) =>
event.preventDefault()
- @gentle_alert "Ban"
+ parent_tr = $(event.target).parent().parent()
+ tr_children = parent_tr.children()
+ action_type = "ban"
+ submission_id = tr_children[4].innerText
+ student_id = tr_children[5].innerText
+ @gentle_alert student_id
+ @post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, @handle_after_action)
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"})
+ handle_after_action: (data) ->
+ @gentle_alert data
+
gentle_alert: (msg) =>
if $('.message-container').length
$('.message-container').remove()
diff --git a/lms/urls.py b/lms/urls.py
index 260f55dd05..e4494e0166 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -289,7 +289,7 @@ if settings.COURSEWARE_ENABLED:
# Open Ended flagged problem list
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems/take_action_on_flag$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/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
From b4c80da22498f36aa6da563e96f0fef5e34dbd2d Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:50:24 -0500
Subject: [PATCH 12/54] Trim whitespace
---
lms/djangoapps/open_ended_grading/views.py | 6 +++++-
lms/static/coffee/src/open_ended/open_ended.coffee | 1 -
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
index a0ef8239f3..984c544d93 100644
--- a/lms/djangoapps/open_ended_grading/views.py
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -25,6 +25,8 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search
+from django.http import HttpResponse, Http404
+
log = logging.getLogger(__name__)
template_imports = {'urllib': urllib}
@@ -306,7 +308,9 @@ def take_action_on_flags(request, course_id):
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")
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index 07b84c8af5..e54198e2aa 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -29,7 +29,6 @@ class OpenEnded
action_type = "ban"
submission_id = tr_children[4].innerText
student_id = tr_children[5].innerText
- @gentle_alert student_id
@post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, @handle_after_action)
post: (cmd, data, callback) ->
From 2affd4760bef18906c858451dcaa8b80af688485 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:53:33 -0500
Subject: [PATCH 13/54] Add in wiring for unflag action
---
lms/static/coffee/src/open_ended/open_ended.coffee | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index e54198e2aa..227cf2fd76 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -20,7 +20,12 @@ class OpenEnded
unflag: (event) =>
event.preventDefault()
- @gentle_alert "Unflag"
+ parent_tr = $(event.target).parent().parent()
+ tr_children = parent_tr.children()
+ action_type = "unflag"
+ submission_id = tr_children[4].innerText
+ student_id = tr_children[5].innerText
+ @post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, @handle_after_action)
ban: (event) =>
event.preventDefault()
From b0e46085586524caf67b7c76b8907ee23403388d Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 30 Jan 2013 21:56:52 -0500
Subject: [PATCH 14/54] Fix callback alert
---
lms/static/coffee/src/open_ended/open_ended.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index 227cf2fd76..45de3a4fcc 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -42,7 +42,7 @@ class OpenEnded
.error => callback({success: false, error: "Error occured while performing this operation"})
handle_after_action: (data) ->
- @gentle_alert data
+ @gentle_alert data.data
gentle_alert: (msg) =>
if $('.message-container').length
From ada9ff7f27925b21da41e26873b39b8f04a61ed0 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 31 Jan 2013 10:45:11 -0500
Subject: [PATCH 15/54] Fix randomization bug in capa.
(Note: capa_problem was still doing randomization internally, but now it does what's actually intended)
---
common/lib/xmodule/xmodule/capa_module.py | 33 ++++++++++++++++-------
1 file changed, 23 insertions(+), 10 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index f33da6e3a4..27bf0c4cb1 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -2,6 +2,7 @@ import cgi
import datetime
import dateutil
import dateutil.parser
+import hashlib
import json
import logging
import traceback
@@ -25,6 +26,22 @@ log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\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):
"""
@@ -138,13 +155,9 @@ class CapaModule(XModule):
if self.rerandomize == 'never':
self.seed = 1
- elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
- # TODO: This line is badly broken:
- # (1) We're passing student ID to xmodule.
- # (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
+ elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
+ # see comment on randomization_bin
+ self.seed = randomization_bin(system.seed, self.location.url)
else:
self.seed = None
@@ -669,18 +682,18 @@ class CapaDescriptor(RawDescriptor):
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
-
+
def get_context(self):
_context = RawDescriptor.get_context(self)
_context.update({'markdown': self.metadata.get('markdown', '')})
return _context
-
+
@property
def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor,self).editable_metadata_fields
if 'markdown' in subset:
- subset.remove('markdown')
+ subset.remove('markdown')
return subset
From 78f9f63466e6ffce90df07385c80378168af74c7 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 31 Jan 2013 11:19:15 -0500
Subject: [PATCH 16/54] Add in notification type for flagged submissions
---
.../open_ended_grading/open_ended_notifications.py | 3 ++-
lms/djangoapps/open_ended_grading/views.py | 6 ++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/lms/djangoapps/open_ended_grading/open_ended_notifications.py b/lms/djangoapps/open_ended_grading/open_ended_notifications.py
index 43259f3e1b..fec893894f 100644
--- a/lms/djangoapps/open_ended_grading/open_ended_notifications.py
+++ b/lms/djangoapps/open_ended_grading/open_ended_notifications.py
@@ -19,7 +19,8 @@ KEY_PREFIX = "open_ended_"
NOTIFICATION_TYPES = (
('student_needs_to_peer_grade', 'peer_grading', 'Peer 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):
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
index 984c544d93..1777f26e2e 100644
--- a/lms/djangoapps/open_ended_grading/views.py
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -56,12 +56,14 @@ def _reverse_without_slash(url_name, course_id):
DESCRIPTION_DICT = {
'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.",
- '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 = {
'Peer 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)
def staff_grading(request, course_id):
From e431378f46a812afc5a562cedd87babe7beb5bec Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 31 Jan 2013 12:14:01 -0500
Subject: [PATCH 17/54] Add first pass at a randomize module
---
common/lib/xmodule/setup.py | 1 +
.../lib/xmodule/xmodule/randomize_module.py | 122 ++++++++++++++++++
2 files changed, 123 insertions(+)
create mode 100644 common/lib/xmodule/xmodule/randomize_module.py
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 29227c3188..446078ffcf 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -28,6 +28,7 @@ setup(
"error = xmodule.error_module:ErrorDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
+ "randomize = xmodule.randomize_module:RandomizeDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
diff --git a/common/lib/xmodule/xmodule/randomize_module.py b/common/lib/xmodule/xmodule/randomize_module.py
new file mode 100644
index 0000000000..0bc26c21bf
--- /dev/null
+++ b/common/lib/xmodule/xmodule/randomize_module.py
@@ -0,0 +1,122 @@
+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:
+
+
+
+
+
+
+ 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)
+
+ log.debug("********* self.choice = %s", self.choice)
+ 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 "Nothing to randomize between "
+
+ 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
+
From e0fb906c0692da41253ac1fe9411c6703f981f44 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 31 Jan 2013 12:14:20 -0500
Subject: [PATCH 18/54] add note about potential bug in verticals. No time to
investigate at the moment...
---
common/lib/xmodule/xmodule/vertical_module.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py
index 397bd3e136..14105b41d0 100644
--- a/common/lib/xmodule/xmodule/vertical_module.py
+++ b/common/lib/xmodule/xmodule/vertical_module.py
@@ -48,3 +48,5 @@ class VerticalDescriptor(SequenceDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
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...
From 63d4ac8c442615c45435b2186dfbaa590891037f Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 31 Jan 2013 12:17:21 -0500
Subject: [PATCH 19/54] Working on some flagging coffeescript
---
.../coffee/src/open_ended/open_ended.coffee | 22 +++++++++++++------
.../open_ended_flagged_problems.html | 3 +++
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index 45de3a4fcc..558d712c46 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -23,26 +23,34 @@ class OpenEnded
parent_tr = $(event.target).parent().parent()
tr_children = parent_tr.children()
action_type = "unflag"
- submission_id = tr_children[4].innerText
- student_id = tr_children[5].innerText
- @post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, @handle_after_action)
+ submission_id = tr_children[5].innerText
+ student_id = tr_children[6].innerText
+ 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 = tr_children[4].innerText
- student_id = tr_children[5].innerText
- @post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, @handle_after_action)
+ submission_id = tr_children[5].innerText
+ student_id = tr_children[6].innerText
+ 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) ->
+ return @handle_after_action
+
handle_after_action: (data) ->
- @gentle_alert data.data
+ tr_parent = target.parent().parent()
+ tr_children = tr_parent.children()
+ action_taken = tr_children[4].children()[0]
+ action_taken.replaceWith('#{action_type} done for student. ')
gentle_alert: (msg) =>
if $('.message-container').length
diff --git a/lms/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
index 9265ad4663..ec892da43c 100644
--- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html
+++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
@@ -47,6 +47,9 @@
Ban
|
+
+
+ |
${problem['submission_id']}
|
From e41172d55df9f1a0cb142b6a59625eef59dfa519 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sun, 20 Jan 2013 11:50:51 -0500
Subject: [PATCH 20/54] Add start of test framework for capa
---
.../xmodule/xmodule/tests/test_capa_module.py | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
create mode 100644 common/lib/xmodule/xmodule/tests/test_capa_module.py
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
new file mode 100644
index 0000000000..148fd893ff
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -0,0 +1,60 @@
+import json
+from mock import Mock
+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 = """
+
+
+ What is pi, to two decimal placs?
+
+
+
+
+
+"""
+
+ num = 0
+ @staticmethod
+ def next_num():
+ CapaFactory.num += 1
+ return CapaFactory.num
+
+ @staticmethod
+ def create():
+ definition = {'data': CapaFactory.sample_problem_xml,}
+ location = Location(["i4x", "edX", "capa_test", "problem",
+ "SampleProblem{0}".format(CapaFactory.next_num())])
+ metadata = {}
+ descriptor = Mock(weight="1")
+ instance_state = None
+
+ module = CapaModule(test_system, location,
+ definition, descriptor,
+ instance_state, None, metadata=metadata)
+
+ return module
+
+
+
+class CapaModuleTest(unittest.TestCase):
+
+ 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")
+
From 025b074b87b5fc60c712292d541449d0d470152b Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sun, 20 Jan 2013 12:17:22 -0500
Subject: [PATCH 21/54] Add simple test for showanswer, fix test_system
---
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../xmodule/xmodule/tests/test_capa_module.py | 60 ++++++++++++++++++-
2 files changed, 59 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index a07f1ddfaf..1f323834a9 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -26,7 +26,7 @@ test_system = ModuleSystem(
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
- user=Mock(),
+ user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 148fd893ff..7537cb537c 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,7 +1,9 @@
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
@@ -31,13 +33,59 @@ class CapaFactory(object):
return CapaFactory.num
@staticmethod
- def create():
+ 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. Should be a number.
+ """
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 = None
+ instance_state_dict = {}
+ if problem_state is not None:
+ instance_state_dict = problem_state
+ if attempts is not None:
+ instance_state_dict['attempts'] = 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,
@@ -58,3 +106,11 @@ class CapaModuleTest(unittest.TestCase):
self.assertNotEqual(module.url_name, other_module.url_name,
"Factory should be creating unique names for each problem")
+ def test_showanswer(self):
+ """
+ Make sure the show answer logic does the right thing.
+ """
+ # default, no due date, showanswer 'closed'
+ problem = CapaFactory.create()
+ pprint(problem.__dict__)
+ self.assertFalse(problem.answer_available())
From ea091a6eb83b09fbc5bafbe4f0f5011b69c8db7b Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sun, 20 Jan 2013 12:49:05 -0500
Subject: [PATCH 22/54] Add tests for showanswer
---
.../xmodule/xmodule/tests/test_capa_module.py | 68 +++++++++++++++++--
1 file changed, 62 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 7537cb537c..506c7faf9f 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,9 +1,9 @@
+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
@@ -56,7 +56,7 @@ class CapaFactory(object):
problem_state: a dict to to be serialized into the instance_state of the
module.
- attempts: also added to instance state. Should be a number.
+ 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",
@@ -81,7 +81,9 @@ class CapaFactory(object):
if problem_state is not None:
instance_state_dict = problem_state
if attempts is not None:
- instance_state_dict['attempts'] = attempts
+ # 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:
@@ -97,6 +99,17 @@ class CapaFactory(object):
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)
@@ -106,11 +119,54 @@ class CapaModuleTest(unittest.TestCase):
self.assertNotEqual(module.url_name, other_module.url_name,
"Factory should be creating unique names for each problem")
- def test_showanswer(self):
+ def test_showanswer_default(self):
"""
Make sure the show answer logic does the right thing.
"""
- # default, no due date, showanswer 'closed'
+ # default, no due date, showanswer 'closed', so problem is open, and show_answer
+ # not visible.
problem = CapaFactory.create()
- pprint(problem.__dict__)
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
+ used_all_attempts = CapaFactory.create(showanswer='closed',
+ max_attempts="1",
+ attempts="1")
+ 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())
+
+
+
+
From 6088a926cc0697094c1bd6ae095581895fcc4563 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sun, 20 Jan 2013 17:35:03 -0500
Subject: [PATCH 23/54] Add showanswer="past_due" and tests
---
common/lib/xmodule/xmodule/capa_module.py | 35 ++++++++------
.../xmodule/xmodule/tests/test_capa_module.py | 47 ++++++++++++++++++-
2 files changed, 65 insertions(+), 17 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index f33da6e3a4..6d258e61ed 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -389,38 +389,43 @@ class CapaModule(XModule):
})
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):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
return True
- if self.close_date is not None and datetime.datetime.utcnow() > self.close_date:
+ if self.is_past_due():
return True
return False
def answer_available(self):
- ''' Is the user allowed to see an answer?
+ '''
+ Is the user allowed to see an answer?
'''
if self.show_answer == '':
return False
-
- if self.show_answer == "never":
+ elif self.show_answer == "never":
return False
-
- # Admins can see the answer, unless the problem explicitly prevents it
- if self.system.user_is_staff:
+ elif self.system.user_is_staff:
+ # This i after the 'never' check because admins can see the answer
+ # unless the problem explicitly prevents it
return True
-
- if self.show_answer == 'attempted':
+ elif self.show_answer == 'attempted':
return self.attempts > 0
-
- if self.show_answer == 'answered':
+ elif self.show_answer == 'answered':
return self.lcp.done
-
- if self.show_answer == 'closed':
+ elif self.show_answer == 'closed':
return self.closed()
-
- if self.show_answer == 'always':
+ elif self.show_answer == 'past_due':
+ return self.is_past_due()
+ elif self.show_answer == 'always':
return True
return False
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 506c7faf9f..e8f639e3c9 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -138,10 +138,11 @@ class CapaModuleTest(unittest.TestCase):
def test_showanswer_closed(self):
- # can see after attempts used up
+ # 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")
+ attempts="1",
+ due=self.tomorrow_str)
self.assertTrue(used_all_attempts.answer_available())
@@ -152,6 +153,7 @@ class CapaModuleTest(unittest.TestCase):
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",
@@ -169,4 +171,45 @@ class CapaModuleTest(unittest.TestCase):
+ 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())
+
+
+
+
From f3f509da3b7a63b9d5a14939c02f9a9780104337 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 31 Jan 2013 12:45:48 -0500
Subject: [PATCH 24/54] Fix input area styling
---
.../xmodule/xmodule/css/combinedopenended/display.scss | 5 +++--
.../xmodule/js/src/combinedopenended/display.coffee | 2 +-
lms/static/coffee/src/open_ended/open_ended.coffee | 9 +++++----
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
index 41896e6173..38fd6ba01c 100644
--- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
+++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
@@ -442,12 +442,13 @@ section.open-ended-child {
margin: 10px;
}
- span.short-form-response {
- padding: 9px;
+ div.short-form-response {
background: #F6F6F6;
border: 1px solid #ddd;
border-top: 0;
margin-bottom: 20px;
+ overflow-y: auto;
+ height: 200px;
@include clearfix;
}
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
index 2aabd35771..89954deb23 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -351,5 +351,5 @@ class @CombinedOpenEnded
answer_id = @answer_area.attr('id')
answer_val = @answer_area.val()
new_text = ''
- new_text = "#{answer_val}"
+ new_text = "#{answer_val} "
@answer_area.replaceWith(new_text)
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
index 558d712c46..aff1e5fc67 100644
--- a/lms/static/coffee/src/open_ended/open_ended.coffee
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -44,13 +44,14 @@ class OpenEnded
.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) ->
- tr_parent = target.parent().parent()
- tr_children = tr_parent.children()
- action_taken = tr_children[4].children()[0]
- action_taken.replaceWith('#{action_type} done for student. ')
+ blah = "blah"
gentle_alert: (msg) =>
if $('.message-container').length
From 1595bd9b0eb82df1bc68b7813f642c809ab67844 Mon Sep 17 00:00:00 2001
From: Don Mitchell
Date: Fri, 1 Feb 2013 17:20:58 -0500
Subject: [PATCH 25/54] Move dnd code from base.js to a js file only loaded by
overview.html
---
cms/djangoapps/contentstore/views.py | 1 -
cms/static/js/base.js | 188 --------------------------
cms/static/js/views/overview.js | 191 +++++++++++++++++++++++++++
cms/templates/overview.html | 1 +
4 files changed, 192 insertions(+), 189 deletions(-)
create mode 100644 cms/static/js/views/overview.js
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 14f96e312a..f70164138d 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -261,7 +261,6 @@ def edit_unit(request, location):
break
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)
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 41c1ee3cdb..7e55d2b8d8 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -80,64 +80,6 @@ $(document).ready(function() {
$('.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);
// section name editing
@@ -279,136 +221,6 @@ function removePolicyMetadata(e) {
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) {
var edxTimeStr = null;
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
new file mode 100644
index 0000000000..c007ef3efc
--- /dev/null
+++ b/cms/static/js/views/overview.js
@@ -0,0 +1,191 @@
+$(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,
+ 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
+ });
+
+});
+
+
+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})
+ });
+
+}
diff --git a/cms/templates/overview.html b/cms/templates/overview.html
index a20531200e..20ddcead01 100644
--- a/cms/templates/overview.html
+++ b/cms/templates/overview.html
@@ -18,6 +18,7 @@
+
|