From 5f538f078b4e7b02a3f328d114635971205267eb Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 19:50:36 -0500 Subject: [PATCH 01/54] Pass flag through from LMS to grading controller --- .../open_ended_grading/peer_grading_service.py | 10 ++++++---- .../src/peer_grading/peer_grading_problem.coffee | 8 ++++++-- lms/templates/peer_grading/peer_grading_problem.html | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index caa349125d..76f54bb12c 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -88,7 +88,7 @@ class PeerGradingService(GradingService): {'location': problem_location, 'grader_id': grader_id}) 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, 'submission_id' : submission_id, 'score' : score, @@ -96,7 +96,8 @@ class PeerGradingService(GradingService): 'submission_key': submission_key, 'location': location, 'rubric_scores': rubric_scores, - 'rubric_scores_complete': True} + 'rubric_scores_complete': True, + 'submission_flagged' : submission_flagged} return self.post(self.save_grade_url, data) def is_student_calibrated(self, problem_location, grader_id): @@ -224,7 +225,7 @@ def save_grade(request, course_id): error: if there was an error in the submission, this is the error message """ _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) if not success: return _err_response(message) @@ -236,9 +237,10 @@ def save_grade(request, course_id): feedback = p['feedback'] submission_key = p['submission_key'] rubric_scores = p.getlist('rubric_scores[]') + submission_flagged = p['submission_flagged'] try: 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") except GradingServiceError: log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2}, diff --git a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee index c4b87eb30e..ab16b34d12 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -175,6 +175,7 @@ class PeerGradingProblem @submission_container = $('.submission-container') @prompt_container = $('.prompt-container') @rubric_container = $('.rubric-container') + @flag_student_container = $('.flag-student-container') @calibration_panel = $('.calibration-panel') @grading_panel = $('.grading-panel') @content_panel = $('.content-panel') @@ -201,6 +202,7 @@ class PeerGradingProblem @action_button = $('.action-button') @calibration_feedback_button = $('.calibration-feedback-button') @interstitial_page_button = $('.interstitial-page-button') + @flag_student_checkbox = $('.flag-checkbox') Collapsible.setCollapsibles(@content_panel) @@ -252,7 +254,8 @@ class PeerGradingProblem location: @location submission_id: @essay_id_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 @@ -352,7 +355,7 @@ class PeerGradingProblem @grading_panel.find('.calibration-text').show() @calibration_panel.find('.grading-text').hide() @grading_panel.find('.grading-text').hide() - + @flag_student_container.hide() @submit_button.unbind('click') @submit_button.click @submit_calibration_essay @@ -379,6 +382,7 @@ class PeerGradingProblem @grading_panel.find('.calibration-text').hide() @calibration_panel.find('.grading-text').show() @grading_panel.find('.grading-text').show() + @flag_student_container.show() @submit_button.unbind('click') @submit_button.click @submit_grade diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index cb9ed1c0fb..04ee7415ec 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -72,6 +72,7 @@

+

Flag this submission for review by course staff (use if the submission contains inappropriate content):

From 78696c62b8ae3268d5cd73eb6a6f6db2d987d7c5 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 20:36:09 -0500 Subject: [PATCH 02/54] Work on open ended flagged problems --- .../controller_query_service.py | 20 +++++++ lms/djangoapps/open_ended_grading/views.py | 55 +++++++++++++++++++ .../open_ended_flagged_problems.html | 52 ++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 lms/templates/open_ended_problems/open_ended_flagged_problems.html diff --git a/lms/djangoapps/open_ended_grading/controller_query_service.py b/lms/djangoapps/open_ended_grading/controller_query_service.py index 7d515e2475..7c75d44287 100644 --- a/lms/djangoapps/open_ended_grading/controller_query_service.py +++ b/lms/djangoapps/open_ended_grading/controller_query_service.py @@ -21,6 +21,8 @@ class ControllerQueryService(GradingService): self.is_unique_url = self.url + '/is_name_unique/' self.combined_notifications_url = self.url + '/combined_notifications/' 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): params = { @@ -57,3 +59,21 @@ class ControllerQueryService(GradingService): response = self.get(self.grading_status_list_url, params) 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): + params = { + 'course_id' : course_id, + 'student_id' : student_id, + 'submission_id' : submission_id, + } + + response = self.post(self.take_action_on_flags_url, params) + return response diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 2ebd8778e6..fd43921761 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -193,6 +193,61 @@ def student_problem_list(request, course_id): # Checked above 'staff_access': False, }) +@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_grading_status_list(course_id, unique_id_for_user(request.user)) + 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 = problem_list_dict['problem_list'] + + for i in xrange(0,len(problem_list)): + problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) + problem_url = base_course_url + "/" + for z in xrange(0,len(problem_url_parts)): + part = problem_url_parts[z] + if part is not None: + if z==1: + problem_url += "courseware/" + problem_url += part + "/" + + problem_list[i].update({'actual_url' : problem_url}) + + 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_problems', course_id) + + return render_to_response('open_ended_problems/open_ended_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': False, }) + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def combined_notifications(request, course_id): course = get_course_with_access(request.user, course_id, 'load') diff --git a/lms/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html new file mode 100644 index 0000000000..447a34ff45 --- /dev/null +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -0,0 +1,52 @@ +<%inherit file="/main.html" /> +<%block name="bodyclass">${course.css_class} +<%namespace name='static' file='/static_content.html'/> + +<%block name="headextra"> +<%static:css group='course'/> + + +<%block name="title">${course.number} Flagged Open Ended Problems + +<%include file="/courseware/course_navigation.html" args="active_page='flagged_open_ended_problems'" /> + +
+
+
${error_text}
+

Flagged Open Ended Problems

+

Instructions

+

Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.

+ % if success: + % if len(flagged_list) == 0: +
+ No flagged problems exist. +
+ %else: + + + + + + + + %for problem in problem_list: + + + + + + + %endfor +
Problem NameStudent IDStudent ResponseSubmission ID
+ ${problem['problem_name']} + + ${problem['student_id']} + + ${problem['student_response']} + + ${problem['submission_id']} +
+ %endif + %endif +
+
From dd72297f73ed9af5e22a8703709a25cd53bb74e6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 20:39:53 -0500 Subject: [PATCH 03/54] Wire up flagged problem list --- lms/djangoapps/open_ended_grading/views.py | 21 ++++--------------- .../open_ended_flagged_problems.html | 2 +- lms/urls.py | 4 ++++ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index fd43921761..6a4131dc6f 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -208,25 +208,13 @@ def flagged_problem_list(request, course_id): base_course_url = reverse('courses') try: - problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) + 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 = problem_list_dict['problem_list'] - - for i in xrange(0,len(problem_list)): - problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) - problem_url = base_course_url + "/" - for z in xrange(0,len(problem_url_parts)): - part = problem_url_parts[z] - if part is not None: - if z==1: - problem_url += "courseware/" - problem_url += part + "/" - - problem_list[i].update({'actual_url' : problem_url}) + problem_list = problem_list_dict['flagged_submissions'] except GradingServiceError: error_text = "Error occured while contacting the grading service" @@ -238,7 +226,7 @@ def flagged_problem_list(request, course_id): ajax_url = _reverse_with_slash('open_ended_problems', course_id) - return render_to_response('open_ended_problems/open_ended_problems.html', { + return render_to_response('open_ended_problems/open_ended_flagged_problems.html', { 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, @@ -246,14 +234,13 @@ def flagged_problem_list(request, course_id): 'problem_list': problem_list, 'error_text': error_text, # Checked above - 'staff_access': False, }) + 'staff_access': True, }) @cache_control(no_cache=True, no_store=True, must_revalidate=True) def combined_notifications(request, course_id): course = get_course_with_access(request.user, course_id, 'load') user = request.user notifications = open_ended_notifications.combined_notifications(course, user) - log.debug(notifications) response = notifications['response'] notification_tuples=open_ended_notifications.NOTIFICATION_TYPES 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 447a34ff45..054c028071 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -8,7 +8,7 @@ <%block name="title">${course.number} Flagged Open Ended Problems -<%include file="/courseware/course_navigation.html" args="active_page='flagged_open_ended_problems'" /> +<%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" />
diff --git a/lms/urls.py b/lms/urls.py index 4b3cc94cab..f122635821 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -285,6 +285,10 @@ if settings.COURSEWARE_ENABLED: # Open Ended problem list url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_problems$', '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'), # Cohorts management url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts$', From b6f77f6f8e8f1ba5d2cab867d50b7baf3e7e9334 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 20:43:57 -0500 Subject: [PATCH 04/54] Fix rendering for flagged problems view --- .../open_ended_problems/open_ended_flagged_problems.html | 4 ++++ 1 file changed, 4 insertions(+) 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 054c028071..bf70906637 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -28,6 +28,7 @@ Student ID Student Response Submission ID + Location %for problem in problem_list: @@ -43,6 +44,9 @@ ${problem['submission_id']} + + ${problem['location']} + %endfor From e89196e0dd89bed02de4b153c74e7426088de2c0 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 20:47:00 -0500 Subject: [PATCH 05/54] Add in flagged problem list return --- lms/djangoapps/open_ended_grading/views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 6a4131dc6f..f1a9d41974 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -158,8 +158,9 @@ def student_problem_list(request, course_id): success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] - - problem_list = problem_list_dict['problem_list'] + problem_list = [] + else: + problem_list = problem_list_dict['problem_list'] for i in xrange(0,len(problem_list)): problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) @@ -213,8 +214,9 @@ def flagged_problem_list(request, course_id): success = problem_list_dict['success'] if 'error' in problem_list_dict: error_text = problem_list_dict['error'] - - problem_list = problem_list_dict['flagged_submissions'] + problem_list=[] + else: + problem_list = problem_list_dict['flagged_submissions'] except GradingServiceError: error_text = "Error occured while contacting the grading service" From f89e36b5b9bd7fc21fa26f636635f39804c0019e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 20:54:02 -0500 Subject: [PATCH 06/54] Flagged list renders properly --- .../open_ended_flagged_problems.html | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 bf70906637..eb7b34d8e6 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -17,18 +17,17 @@

Instructions

Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.

% if success: - % if len(flagged_list) == 0: + % if len(problem_list) == 0:
No flagged problems exist.
%else: - + - - - + + %for problem in problem_list: @@ -38,14 +37,11 @@ - %endfor From b248a0957254071c4b0db85cd1aaa0300fe74e8e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 21:06:54 -0500 Subject: [PATCH 07/54] Work on adding in ban actions --- .../controller_query_service.py | 4 ++- .../peer_grading_service.py | 2 +- lms/djangoapps/open_ended_grading/views.py | 27 +++++++++++++++++++ .../open_ended_flagged_problems.html | 16 +++++++---- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/controller_query_service.py b/lms/djangoapps/open_ended_grading/controller_query_service.py index 7c75d44287..d40c9b4428 100644 --- a/lms/djangoapps/open_ended_grading/controller_query_service.py +++ b/lms/djangoapps/open_ended_grading/controller_query_service.py @@ -68,12 +68,14 @@ class ControllerQueryService(GradingService): response = self.get(self.flagged_problem_list_url, params) return response - def take_action_on_flags(self, course_id, student_id, submission_id): + 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 + diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 76f54bb12c..994ba0b2be 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -377,4 +377,4 @@ def save_calibration_essay(request, course_id): 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') + return _err_response('Could not connect to grading service') \ No newline at end of file diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index f1a9d41974..717a33ec61 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -287,5 +287,32 @@ def combined_notifications(request, course_id): return render_to_response('open_ended_problems/combined_notifications.html', combined_dict ) + +def take_action_on_flags(request, course_id): + """ + + """ + 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'] + + try: + controller_qs = ControllerQueryService() + response = controller_qs.save_calibration_essay(course_id, student_id, course_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') 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 eb7b34d8e6..97f1e03c18 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -25,9 +25,9 @@
Problem NameName Student IDStudent ResponseSubmission IDLocationIDResponse
${problem['student_id']} - ${problem['student_response']} - ${problem['submission_id']} - ${problem['location']} + ${problem['student_response']}
- - + + %for problem in problem_list: @@ -35,13 +35,19 @@ ${problem['problem_name']} + + - %endfor From 17fce100bb0a41e43a2c8aed6127c9f3e7cb0ed3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 30 Jan 2013 21:10:48 -0500 Subject: [PATCH 08/54] Add in ban actions to table --- lms/djangoapps/open_ended_grading/views.py | 2 +- .../open_ended_problems/open_ended_flagged_problems.html | 8 ++++---- lms/urls.py | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 717a33ec61..aaee9f4c98 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.save_calibration_essay(course_id, student_id, course_id, action_type) + response = controller_qs.take_action_on_flags(course_id, student_id, course_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/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html index 97f1e03c18..2397e1a70e 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -26,8 +26,8 @@ - - + + %for problem in problem_list: @@ -38,10 +38,10 @@ ${problem['student_response']} + 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 @@ +
NameStudent IDID ResponseUnflagBan
- ${problem['student_id']} + ${problem['student_response']} + + + + ${problem['submission_id']} - ${problem['student_response']} + + ${problem['student_id']}
Name ResponseUnflagBan
- + 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'/> + +
${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']}