From ecf04b3e493ff5c9c3f14de98f691cb3ec086576 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 28 Dec 2012 11:29:29 -0500 Subject: [PATCH 001/329] Refactor existing grading logic into a new app. --- lms/djangoapps/instructor/views.py | 21 ------ lms/djangoapps/open_ended_grading/__init__.py | 0 .../open_ended_grading/grading_service.py | 71 +++++++++++++++++++ .../peer_grading_service.py | 15 ++++ .../staff_grading.py} | 0 .../staff_grading_service.py | 69 +++--------------- lms/djangoapps/open_ended_grading/tests.py | 16 +++++ lms/djangoapps/open_ended_grading/views.py | 57 +++++++++++++++ lms/envs/common.py | 1 + lms/urls.py | 10 +-- 10 files changed, 176 insertions(+), 84 deletions(-) create mode 100644 lms/djangoapps/open_ended_grading/__init__.py create mode 100644 lms/djangoapps/open_ended_grading/grading_service.py create mode 100644 lms/djangoapps/open_ended_grading/peer_grading_service.py rename lms/djangoapps/{instructor/grading.py => open_ended_grading/staff_grading.py} (100%) rename lms/djangoapps/{instructor => open_ended_grading}/staff_grading_service.py (84%) create mode 100644 lms/djangoapps/open_ended_grading/tests.py create mode 100644 lms/djangoapps/open_ended_grading/views.py diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 79cf0caaf3..2bad058ad8 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -28,7 +28,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -from .grading import StaffGrading log = logging.getLogger(__name__) @@ -414,26 +413,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -def staff_grading(request, course_id): - """ - Show the instructor grading interface. - """ - course = get_course_with_access(request.user, course_id, 'staff') - - grading = StaffGrading(course) - - ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) - if not ajax_url.endswith('/'): - ajax_url += '/' - - return render_to_response('instructor/staff_grading.html', { - 'view_html': grading.get_html(), - 'course': course, - 'course_id': course_id, - 'ajax_url': ajax_url, - # Checked above - 'staff_access': True, }) @cache_control(no_cache=True, no_store=True, must_revalidate=True) diff --git a/lms/djangoapps/open_ended_grading/__init__.py b/lms/djangoapps/open_ended_grading/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/open_ended_grading/grading_service.py b/lms/djangoapps/open_ended_grading/grading_service.py new file mode 100644 index 0000000000..be15ae08ee --- /dev/null +++ b/lms/djangoapps/open_ended_grading/grading_service.py @@ -0,0 +1,71 @@ +# This class gives a common interface for logging into +# the graing controller +import json +import logging +import requests +from requests.exceptions import RequestException, ConnectionError, HTTPError +import sys + +from django.conf import settings +from django.http import HttpResponse, Http404 + +from courseware.access import has_access +from util.json_request import expect_json +from xmodule.course_module import CourseDescriptor + +log = logging.getLogger(__name__) + +class GradingServiceError(Exception): + pass + +class GradingService(object): + """ + Interface to staff grading backend. + """ + def __init__(self, config): + self.username = config['username'] + self.password = config['password'] + self.url = config['url'] + self.login_url = self.url + '/login/' + self.session = requests.session() + + def _login(self): + """ + Log into the staff grading service. + + Raises requests.exceptions.HTTPError if something goes wrong. + + Returns the decoded json dict of the response. + """ + response = self.session.post(self.login_url, + {'username': self.username, + 'password': self.password,}) + + response.raise_for_status() + + return response.json + + + def _try_with_login(self, operation): + """ + Call operation(), which should return a requests response object. If + the request fails with a 'login_required' error, call _login() and try + the operation again. + + Returns the result of operation(). Does not catch exceptions. + """ + response = operation() + if (response.json + and response.json.get('success') == False + and response.json.get('error') == 'login_required'): + # apparrently we aren't logged in. Try to fix that. + r = self._login() + if r and not r.get('success'): + log.warning("Couldn't log into staff_grading backend. Response: %s", + r) + # try again + response = operation() + response.raise_for_status() + + return response + diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py new file mode 100644 index 0000000000..cad23a072c --- /dev/null +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -0,0 +1,15 @@ +import json +import logging +import requests +from requests.exceptions import RequestException, ConnectionError, HTTPError +import sys + +from django.conf import settings +from django.http import HttpResponse, Http404 + +from courseware.access import has_access +from util.json_request import expect_json +from xmodule.course_module import CourseDescriptor + +log = logging.getLogger(__name__) + diff --git a/lms/djangoapps/instructor/grading.py b/lms/djangoapps/open_ended_grading/staff_grading.py similarity index 100% rename from lms/djangoapps/instructor/grading.py rename to lms/djangoapps/open_ended_grading/staff_grading.py diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py similarity index 84% rename from lms/djangoapps/instructor/staff_grading_service.py rename to lms/djangoapps/open_ended_grading/staff_grading_service.py index ea8f0de074..6d0cea983b 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -7,6 +7,8 @@ import logging import requests from requests.exceptions import RequestException, ConnectionError, HTTPError import sys +from grading_service import GradingService +from grading_service import GradingServiceError from django.conf import settings from django.http import HttpResponse, Http404 @@ -18,9 +20,6 @@ from xmodule.course_module import CourseDescriptor log = logging.getLogger(__name__) -class GradingServiceError(Exception): - pass - class MockStaffGradingService(object): """ @@ -57,62 +56,16 @@ class MockStaffGradingService(object): return self.get_next(course_id, 'fake location', grader_id) -class StaffGradingService(object): +class StaffGradingService(GradingService): """ Interface to staff grading backend. """ def __init__(self, config): - self.username = config['username'] - self.password = config['password'] - self.url = config['url'] - - self.login_url = self.url + '/login/' + super(StaffGradingService, self).__init__(config) self.get_next_url = self.url + '/get_next_submission/' self.save_grade_url = self.url + '/save_grade/' self.get_problem_list_url = self.url + '/get_problem_list/' - self.session = requests.session() - - - def _login(self): - """ - Log into the staff grading service. - - Raises requests.exceptions.HTTPError if something goes wrong. - - Returns the decoded json dict of the response. - """ - response = self.session.post(self.login_url, - {'username': self.username, - 'password': self.password,}) - - response.raise_for_status() - - return response.json - - - def _try_with_login(self, operation): - """ - Call operation(), which should return a requests response object. If - the request fails with a 'login_required' error, call _login() and try - the operation again. - - Returns the result of operation(). Does not catch exceptions. - """ - response = operation() - if (response.json - and response.json.get('success') == False - and response.json.get('error') == 'login_required'): - # apparrently we aren't logged in. Try to fix that. - r = self._login() - if r and not r.get('success'): - log.warning("Couldn't log into staff_grading backend. Response: %s", - r) - # try again - response = operation() - response.raise_for_status() - - return response def get_problem_list(self, course_id, grader_id): """ @@ -203,11 +156,11 @@ class StaffGradingService(object): return r.text -# don't initialize until grading_service() is called--means that just +# don't initialize until staff_grading_service() is called--means that just # importing this file doesn't create objects that may not have the right config _service = None -def grading_service(): +def staff_grading_service(): """ Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True, returns a mock one, otherwise a real one. @@ -308,12 +261,12 @@ def get_problem_list(request, course_id): """ _check_access(request.user, course_id) try: - response = grading_service().get_problem_list(course_id, request.user.id) + response = staff_grading_service().get_problem_list(course_id, request.user.id) return HttpResponse(response, mimetype="application/json") except GradingServiceError: log.exception("Error from grading service. server url: {0}" - .format(grading_service().url)) + .format(staff_grading_service().url)) return HttpResponse(json.dumps({'success': False, 'error': 'Could not connect to grading service'})) @@ -323,10 +276,10 @@ def _get_next(course_id, grader_id, location): Implementation of get_next (also called from save_grade) -- returns a json string """ try: - return grading_service().get_next(course_id, location, grader_id) + return staff_grading_service().get_next(course_id, location, grader_id) except GradingServiceError: log.exception("Error from grading service. server url: {0}" - .format(grading_service().url)) + .format(staff_grading_service().url)) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -364,7 +317,7 @@ def save_grade(request, course_id): location = p['location'] skipped = 'skipped' in p try: - result_json = grading_service().save_grade(course_id, + result_json = staff_grading_service().save_grade(course_id, grader_id, p['submission_id'], p['score'], diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py new file mode 100644 index 0000000000..9066f8323a --- /dev/null +++ b/lms/djangoapps/open_ended_grading/views.py @@ -0,0 +1,57 @@ +# Grading Views + +from collections import defaultdict +import csv +import logging +import os +import urllib + +from django.conf import settings +from django.contrib.auth.models import User, Group +from django.http import HttpResponse +from django_future.csrf import ensure_csrf_cookie +from django.views.decorators.cache import cache_control +from mitxmako.shortcuts import render_to_response +from django.core.urlresolvers import reverse + +from courseware import grades +from courseware.access import has_access, get_access_group_name +from courseware.courses import get_course_with_access +from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA +from django_comment_client.utils import has_forum_access +from psychometrics import psychoanalyze +from student.models import CourseEnrollment +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem +from xmodule.modulestore.search import path_to_location +import track.views + +from .staff_grading import StaffGrading + + +log = logging.getLogger(__name__) + +template_imports = {'urllib': urllib} +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def staff_grading(request, course_id): + """ + Show the instructor grading interface. + """ + course = get_course_with_access(request.user, course_id, 'staff') + + grading = StaffGrading(course) + + ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + + return render_to_response('instructor/staff_grading.html', { + 'view_html': grading.get_html(), + 'course': course, + 'course_id': course_id, + 'ajax_url': ajax_url, + # Checked above + 'staff_access': True, }) + diff --git a/lms/envs/common.py b/lms/envs/common.py index 26941f7e01..1cf22a6323 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -600,6 +600,7 @@ INSTALLED_APPS = ( 'util', 'certificates', 'instructor', + 'open_ended_grading', 'psychometrics', 'licenses', diff --git a/lms/urls.py b/lms/urls.py index baa720028b..f04af88e72 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -241,15 +241,15 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/enroll_students$', 'instructor.views.enroll_students', name='enroll_students'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading$', - 'instructor.views.staff_grading', name='staff_grading'), + 'open_ended_grading.views.staff_grading', name='staff_grading'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_next$', - 'instructor.staff_grading_service.get_next', name='staff_grading_get_next'), + 'open_ended_grading.staff_grading_service.get_next', name='staff_grading_get_next'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$', - 'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'), + 'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$', - 'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'), + 'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$', - 'instructor.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), + 'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), ) # discussion forums live within courseware, so courseware must be enabled first From 125945de9019adf7b989c1f65f48e92937801741 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 28 Dec 2012 13:34:07 -0500 Subject: [PATCH 002/329] Refactor get and post logic into new GradingService --- .../open_ended_grading/grading_service.py | 32 ++++++++++++- .../staff_grading_service.py | 45 +++++-------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/grading_service.py b/lms/djangoapps/open_ended_grading/grading_service.py index be15ae08ee..3c92c5bddd 100644 --- a/lms/djangoapps/open_ended_grading/grading_service.py +++ b/lms/djangoapps/open_ended_grading/grading_service.py @@ -1,5 +1,4 @@ -# This class gives a common interface for logging into -# the graing controller +# This class gives a common interface for logging into the grading controller import json import logging import requests @@ -45,6 +44,35 @@ class GradingService(object): return response.json + def post(self, url, allow_redirects, data): + """ + Make a post request to the grading controller + """ + try: + op = lambda: self.session.post(url, data=data, + allow_redirects=allow_redirects) + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + + def get(self, url, allow_redirects, params): + """ + Make a get request to the grading controller + """ + op = lambda: self.session.get(url, + allow_redirects=allow_redirects, + params=params) + try: + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + def _try_with_login(self, operation): """ diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 6d0cea983b..5d56a90064 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -83,17 +83,8 @@ class StaffGradingService(GradingService): Raises: GradingServiceError: something went wrong with the connection. """ - op = lambda: self.session.get(self.get_problem_list_url, - allow_redirects = False, - params={'course_id': course_id, - 'grader_id': grader_id}) - try: - r = self._try_with_login(op) - except (RequestException, ConnectionError, HTTPError) as err: - # reraise as promised GradingServiceError, but preserve stacktrace. - raise GradingServiceError, str(err), sys.exc_info()[2] - - return r.text + params = {'course_id': course_id,'grader_id': grader_id} + return self.get(self.get_problem_list_url, False, params) def get_next(self, course_id, location, grader_id): @@ -114,17 +105,10 @@ class StaffGradingService(GradingService): Raises: GradingServiceError: something went wrong with the connection. """ - op = lambda: self.session.get(self.get_next_url, + return self.get(self.get_next_url, allow_redirects=False, params={'location': location, 'grader_id': grader_id}) - try: - r = self._try_with_login(op) - except (RequestException, ConnectionError, HTTPError) as err: - # reraise as promised GradingServiceError, but preserve stacktrace. - raise GradingServiceError, str(err), sys.exc_info()[2] - - return r.text def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): @@ -139,22 +123,15 @@ class StaffGradingService(GradingService): Raises: GradingServiceError if there's a problem connecting. """ - try: - data = {'course_id': course_id, - 'submission_id': submission_id, - 'score': score, - 'feedback': feedback, - 'grader_id': grader_id, - 'skipped': skipped} + data = {'course_id': course_id, + 'submission_id': submission_id, + 'score': score, + 'feedback': feedback, + 'grader_id': grader_id, + 'skipped': skipped} - op = lambda: self.session.post(self.save_grade_url, data=data, - allow_redirects=False) - r = self._try_with_login(op) - except (RequestException, ConnectionError, HTTPError) as err: - # reraise as promised GradingServiceError, but preserve stacktrace. - raise GradingServiceError, str(err), sys.exc_info()[2] - - return r.text + return self.post(self.save_grade_url, data=data, + allow_redirects=False) # don't initialize until staff_grading_service() is called--means that just # importing this file doesn't create objects that may not have the right config From d23eb93fbb159c91ec2bf6a66e0825622a566f04 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 28 Dec 2012 13:35:35 -0500 Subject: [PATCH 003/329] add new peer grading service with some rudimentary logic --- .../peer_grading_service.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index cad23a072c..e3b2e823a7 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -6,6 +6,8 @@ import sys from django.conf import settings from django.http import HttpResponse, Http404 +from grading_service import GradingService +from grading_service import GradingServiceError from courseware.access import has_access from util.json_request import expect_json @@ -13,3 +15,26 @@ from xmodule.course_module import CourseDescriptor log = logging.getLogger(__name__) +class PeerGradingService(GradingService): + """ + Interface with the grading controller for peer grading + """ + def __init__(self, config): + super(PeerGradingService, self).__init__(config) + self.get_next_submission_url = self.url + '/get_next_submission/' + self.save_grade_url = self.url + '/save_grade/' + self.is_student_calibrated_url = self.url + '/is_student_calibrated/' + self.show_calibration_essay = self.url + '/show_calibration_essay/' + self.save_calibration_essay = self.url + '/save_calibration_essay/' + + def get_next_submission(self, problem_location, grader_id): + return self.get(self.get_next_submission_url, False, + {'location': problem_location, 'grader_id': grader_id}) + + def save_grade(self, grader_id, submission_id, score, feedback, submission_key): + data = {'grader_id' : grader_id, + 'submission_id' : submission_id, + 'score' : score, + 'feedback' : feedback, + 'submission_key', submission_key} + return self.post(self.save_grade_url, False, data) From 652a8eb440af46e2f377b7970c5e0faea2a96f49 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 28 Dec 2012 15:48:06 -0500 Subject: [PATCH 004/329] New peer grading view on the lms side --- .../peer_grading_service.py | 56 ++++++++++++++++++- lms/djangoapps/open_ended_grading/views.py | 22 +++++++- lms/envs/common.py | 6 +- lms/templates/instructor/staff_grading.html | 4 ++ lms/templates/peer_grading/peer_grading.html | 22 ++++++++ lms/urls.py | 5 ++ 6 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 lms/templates/peer_grading/peer_grading.html diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index e3b2e823a7..b2b1fab5d3 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -36,5 +36,59 @@ class PeerGradingService(GradingService): 'submission_id' : submission_id, 'score' : score, 'feedback' : feedback, - 'submission_key', submission_key} + 'submission_key': submission_key} return self.post(self.save_grade_url, False, data) + + +def peer_grading_service(): + """ + Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True, + returns a mock one, otherwise a real one. + + Caches the result, so changing the setting after the first call to this + function will have no effect. + """ + global _service + if _service is not None: + return _service + + _service = PeerGradingService(settings.PEER_GRADING_INTERFACE) + + return _service + +def _err_response(msg): + """ + Return a HttpResponse with a json dump with success=False, and the given error message. + """ + return HttpResponse(json.dumps({'success': False, 'error': msg}), + mimetype="application/json") + +def get_next_submission(request, course_id): + required = set(['location']) + if request.method != 'POST': + raise Http404 + actual = set(request.POST.keys()) + missing = required - actual + if len(missing) > 0: + return _err_response('Missing required keys {0}'.format( + ', '.join(missing))) + grader_id = request.user.id + p = request.POST + location = p['location'] + + return HttpResponse(_get_next(course_id, request.user.id, location), + mimetype="application/json") + +def _get_next_submission(course_id, grader_id, location): + """ + Implementation of get_next (also called from save_grade) -- returns a json string + """ + try: + return peer_grading_service().get_next_submission(location, grader_id) + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(staff_grading_service().url)) + return json.dumps({'success': False, + 'error': 'Could not connect to grading service'}) + + diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 9066f8323a..41eb0fbccf 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -48,10 +48,30 @@ def staff_grading(request, course_id): ajax_url += '/' return render_to_response('instructor/staff_grading.html', { - 'view_html': grading.get_html(), + 'view_html': '', 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, # Checked above 'staff_access': True, }) + +def peer_grading(request, course_id): + ''' + Show a peer grading interface + ''' + course = get_course_with_access(request.user, course_id, 'load') + + ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + + return render_to_response('peer_grading/peer_grading.html', { + 'view_html': '', + 'course': course, + 'course_id': course_id, + 'ajax_url': ajax_url, + # Checked above + 'staff_access': False, }) + + diff --git a/lms/envs/common.py b/lms/envs/common.py index 1cf22a6323..d18c82b754 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -418,6 +418,7 @@ main_vendor_js = [ discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee')) staff_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/staff_grading/**/*.coffee')) +peer_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/peer_grading/**/*.coffee')) # Load javascript from all of the available xmodules, and @@ -526,8 +527,11 @@ PIPELINE_JS = { 'staff_grading' : { 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in staff_grading_js], 'output_filename': 'js/staff_grading.js' + }, + 'peer_grading' : { + 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in peer_grading_js], + 'output_filename': 'js/peer_grading.js' } - } PIPELINE_DISABLE_WRAPPER = True diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index 33580c6267..085480a332 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -24,6 +24,8 @@
+ +

Instructions

@@ -35,6 +37,8 @@
+ +

diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html new file mode 100644 index 0000000000..19753c7ad4 --- /dev/null +++ b/lms/templates/peer_grading/peer_grading.html @@ -0,0 +1,22 @@ +<%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} Peer Grading + +<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> + +<%block name="js_extra"> + <%static:js group='peer_grading'/> + + +
+
+
+
+
+
diff --git a/lms/urls.py b/lms/urls.py index f04af88e72..4f5dfeb666 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -250,6 +250,11 @@ if settings.COURSEWARE_ENABLED: 'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$', 'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$', + 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading$', + 'open_ended_grading.views.peer_grading', name='peer_grading'), + ) # discussion forums live within courseware, so courseware must be enabled first From deffa188ff9b9e44a543739f16f98f96834eba55 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 2 Jan 2013 09:43:21 -0500 Subject: [PATCH 005/329] Fix some minor problems and add new key for peer grading. --- lms/envs/dev.py | 7 +++++++ lms/templates/peer_grading/peer_grading.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 0ad42f67d3..058c67fa4d 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -110,6 +110,13 @@ STAFF_GRADING_INTERFACE = { 'password': 'abcd', } +################################# Peer grading config ##################### + +PEER_GRADING_INTERFACE = { + 'url': 'http://127.0.0.1:3033/peer_grading', + 'username': 'lms', + 'password': 'abcd', + } ################################ LMS Migration ################################# MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 19753c7ad4..0254d4cd67 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -15,7 +15,7 @@
-
+
From 37f261f906411508e1d04031cb08d07664278196 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 2 Jan 2013 15:01:59 -0500 Subject: [PATCH 006/329] Move peer grading so that there are the individual problem pages and the problem list page --- .../peer_grading_service.py | 35 ++++++++-- lms/djangoapps/open_ended_grading/views.py | 68 +++++++++++++++++++ .../src/peer_grading/peer_grading.coffee | 9 +++ .../peer_grading/peer_grading_problem.coffee | 17 +++++ lms/static/sass/course/_staff_grading.scss | 3 +- lms/templates/peer_grading/peer_grading.html | 15 +++- .../peer_grading/peer_grading_problem.html | 22 ++++++ lms/urls.py | 2 + 8 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 lms/static/coffee/src/peer_grading/peer_grading.coffee create mode 100644 lms/static/coffee/src/peer_grading/peer_grading_problem.coffee create mode 100644 lms/templates/peer_grading/peer_grading_problem.html diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index b2b1fab5d3..1b78a32c5d 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -24,12 +24,14 @@ class PeerGradingService(GradingService): self.get_next_submission_url = self.url + '/get_next_submission/' self.save_grade_url = self.url + '/save_grade/' self.is_student_calibrated_url = self.url + '/is_student_calibrated/' - self.show_calibration_essay = self.url + '/show_calibration_essay/' - self.save_calibration_essay = self.url + '/save_calibration_essay/' + self.show_calibration_essay_url = self.url + '/show_calibration_essay/' + self.save_calibration_essay_url = self.url + '/save_calibration_essay/' + self.get_problem_list_url = self.url + '/get_problem_list/' def get_next_submission(self, problem_location, grader_id): - return self.get(self.get_next_submission_url, False, + response = self.get(self.get_next_submission_url, False, {'location': problem_location, 'grader_id': grader_id}) + return response def save_grade(self, grader_id, submission_id, score, feedback, submission_key): data = {'grader_id' : grader_id, @@ -39,6 +41,29 @@ class PeerGradingService(GradingService): 'submission_key': submission_key} return self.post(self.save_grade_url, False, data) + def is_student_calibrated(self, problem_location, grader_id): + params = {'problem_id' : problem_location, 'student_id': grader_id} + return self.get(self.is_student_calibrated_url, False, params) + + def show_calibration_essay(self, problem_location, grader_id): + params = {'problem_id' : problem_location, 'student_id': grader_id} + return self.get(self.show_calibration_essay_url, False, params) + + def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): + data = {'location': problem_location, + 'student_id': grader_id, + 'calibration_essay_id': calibration_essay_id, + 'submission_key': submission_key, + 'score': score, + 'feedback': feedback} + return self.post(self.save_calibration_essay_url, False, data) + + def get_problem_list(self, course_id, grader_id): + params = {'course_id': course_id, 'student_id': grader_id} + response = self.get(self.get_problem_list_url, False, params) + log.debug("Response! {0}".format(response)) + return response + def peer_grading_service(): """ @@ -64,6 +89,9 @@ def _err_response(msg): mimetype="application/json") def get_next_submission(request, course_id): + """ + TODO: fill in this documentation + """ required = set(['location']) if request.method != 'POST': raise Http404 @@ -91,4 +119,3 @@ def _get_next_submission(course_id, grader_id, location): return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) - diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 41eb0fbccf..d4967aa0e9 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -26,6 +26,10 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location + +from peer_grading_service import PeerGradingService +from grading_service import GradingServiceError +import json import track.views from .staff_grading import StaffGrading @@ -34,6 +38,8 @@ from .staff_grading import StaffGrading log = logging.getLogger(__name__) template_imports = {'urllib': urllib} +peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def staff_grading(request, course_id): """ @@ -62,6 +68,22 @@ def peer_grading(request, course_id): ''' course = get_course_with_access(request.user, course_id, 'load') + # call problem list service + success = False + error_text = "" + try: + problem_list_text = peer_gs.get_problem_list(course_id, request.user.id) + problem_list_json = json.loads(problem_list_text) + success = problem_list_json['success'] + if 'error' in problem_list_json: + error_text = problem_list_json['error'] + + problem_list = problem_list_json['problem_list'] + + except GradingServiceError: + error_text = "Error occured while contacting the grading service" + success = False + ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) if not ajax_url.endswith('/'): ajax_url += '/' @@ -71,6 +93,52 @@ def peer_grading(request, course_id): '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, }) + + +def peer_grading_problem(request, course_id, problem_location): + ''' + Show individual problem interface + ''' + course = get_course_with_access(request.user, course_id, 'load') + + # TODO: make sure that we show calibration or next submission correctly + # TODO: figure out if we want to make this page pure ajax or not + + problem_info_text = "" + error_text = "" + # if we are still in calibration + + # show a calibration essay + + # else, show an actual problem + try: + problem_info_text = peer_gs.get_next_submission(problem_location, request.user.id) + log.debug(problem_info_text) + problem_info = json.loads(problem_info_text) + success = problem_info['success'] + if 'error' in problem_info: + error_text = problem_info['error'] + except GradingServiceError: + success = False + + + ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + + return render_to_response('peer_grading/peer_grading_problem.html', { + 'view_html': '', + 'course': course, + 'course_id': course_id, + 'success' : success, + 'problem_info': problem_info_text, + 'ajax_url': ajax_url, + 'error_text': error_text, # Checked above 'staff_access': False, }) diff --git a/lms/static/coffee/src/peer_grading/peer_grading.coffee b/lms/static/coffee/src/peer_grading/peer_grading.coffee new file mode 100644 index 0000000000..46f0206bbf --- /dev/null +++ b/lms/static/coffee/src/peer_grading/peer_grading.coffee @@ -0,0 +1,9 @@ +class PeerGrading + constructor: (backend) -> + @problem_list = $('.problem-list') + @error_container = $('.error-container') + @error_container.toggle(not @error_container.is(':empty')) + + +backend = {} +$(document).ready(() -> new PeerGrading(backend)) diff --git a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee new file mode 100644 index 0000000000..461089b79c --- /dev/null +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -0,0 +1,17 @@ +class PeerGradingProblemBackend + constructor: (ajax_url, mock_backend) -> + @mock_backend = mock_backend + +class PeerGradingProblem + constructor: (backend) -> + @error_container = $('.error-container') + + @render_problem() + + render_problem: () -> + # do this when it makes sense + @error_container.toggle(not @error_container.is(':empty')) + + +backend = {} +$(document).ready(() -> new PeerGradingProblem(backend)) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index f1b6c5845d..bb7f6cef45 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -1,4 +1,5 @@ -div.staff-grading { +div.staff-grading, +div.peer-grading{ textarea.feedback-area { height: 75px; margin: 20px; diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 0254d4cd67..b5a1408fde 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -16,7 +16,18 @@
-
-
+
${error_text}
+

Peer Grading

+

Instructions

+

+ % if success: + + %endif
diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html new file mode 100644 index 0000000000..7ea47f04f7 --- /dev/null +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -0,0 +1,22 @@ + +<%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} Peer Grading. + +<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> + +<%block name="js_extra"> + <%static:js group='peer_grading'/> + + +
+
+
${error_text}
+
+
diff --git a/lms/urls.py b/lms/urls.py index 4f5dfeb666..7f30224ad5 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -254,6 +254,8 @@ if settings.COURSEWARE_ENABLED: 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading$', 'open_ended_grading.views.peer_grading', name='peer_grading'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/problem/(?P.*)$', + 'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'), ) From c89ff2ac0ee2245e571819adaa57c402d2ca8f96 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 2 Jan 2013 17:44:58 -0500 Subject: [PATCH 007/329] Basic peer grading view using mocks and some cleanup in the peer grading service --- .../peer_grading_service.py | 48 ++++++++-- lms/djangoapps/open_ended_grading/views.py | 25 +---- .../src/peer_grading/peer_grading.coffee | 7 +- .../peer_grading/peer_grading_problem.coffee | 91 +++++++++++++++++-- lms/templates/peer_grading/peer_grading.html | 2 +- .../peer_grading/peer_grading_problem.html | 54 ++++++++++- lms/urls.py | 9 +- 7 files changed, 191 insertions(+), 45 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 1b78a32c5d..daccdd53af 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -88,18 +88,28 @@ def _err_response(msg): return HttpResponse(json.dumps({'success': False, 'error': msg}), mimetype="application/json") +def _check_required(request, required): + actual = set(request.POST.keys()) + missing = required - actual + if len(missing) > 0: + return False, "Missing required keys: {0}".format(', '.join(missing)) + else: + return True, "" + +def _check_post(request): + if request.method != 'POST': + raise Http404 + + def get_next_submission(request, course_id): """ TODO: fill in this documentation """ + _check_post(request) required = set(['location']) - if request.method != 'POST': - raise Http404 - actual = set(request.POST.keys()) - missing = required - actual - if len(missing) > 0: - return _err_response('Missing required keys {0}'.format( - ', '.join(missing))) + success, message = _check_required(request, required) + if not success: + return _err_response(message) grader_id = request.user.id p = request.POST location = p['location'] @@ -119,3 +129,27 @@ def _get_next_submission(course_id, grader_id, location): return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) + +def show_calibration_essay(request, course_id): + """ + TODO: fill in this documentation + """ + _check_post(request) + + required = set(['location']) + success, message = _check_required(request, required) + if not success: + return _err_response(message) + + grader_id = request.user.id + p = request.POST + location = p['location'] + try: + response = peer_grading_service().show_calibration_essay(location, grader_id) + return HttpResponse(response, mimetype="application/json") + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(staff_grading_service().url)) + return json.dumps({'success': False, + 'error': 'Could not connect to grading service'}) + diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index d4967aa0e9..6d41d47812 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -106,27 +106,6 @@ def peer_grading_problem(request, course_id, problem_location): ''' course = get_course_with_access(request.user, course_id, 'load') - # TODO: make sure that we show calibration or next submission correctly - # TODO: figure out if we want to make this page pure ajax or not - - problem_info_text = "" - error_text = "" - # if we are still in calibration - - # show a calibration essay - - # else, show an actual problem - try: - problem_info_text = peer_gs.get_next_submission(problem_location, request.user.id) - log.debug(problem_info_text) - problem_info = json.loads(problem_info_text) - success = problem_info['success'] - if 'error' in problem_info: - error_text = problem_info['error'] - except GradingServiceError: - success = False - - ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) if not ajax_url.endswith('/'): ajax_url += '/' @@ -134,11 +113,9 @@ def peer_grading_problem(request, course_id, problem_location): return render_to_response('peer_grading/peer_grading_problem.html', { 'view_html': '', 'course': course, + 'problem_location': problem_location, 'course_id': course_id, - 'success' : success, - 'problem_info': problem_info_text, 'ajax_url': ajax_url, - 'error_text': error_text, # Checked above 'staff_access': False, }) diff --git a/lms/static/coffee/src/peer_grading/peer_grading.coffee b/lms/static/coffee/src/peer_grading/peer_grading.coffee index 46f0206bbf..c20944252c 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading.coffee @@ -1,9 +1,10 @@ class PeerGrading constructor: (backend) -> - @problem_list = $('.problem-list') @error_container = $('.error-container') @error_container.toggle(not @error_container.is(':empty')) + @message_container = $('.message-container') + @message_container.toggle(not @message_container.is(':empty')) -backend = {} -$(document).ready(() -> new PeerGrading(backend)) +mock_backend = false +$(document).ready(() -> new PeerGrading(mock_backend)) 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 461089b79c..5b7aef18c7 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -1,17 +1,96 @@ class PeerGradingProblemBackend constructor: (ajax_url, mock_backend) -> @mock_backend = mock_backend + @ajax_url = ajax_url + + post: (cmd, data, callback) -> + if @mock_backend + callback(@mock(cmd, data)) + else + # TODO: replace with postWithPrefix when that's loaded + $.post(@ajax_url + cmd, data, callback) + .error => callback({success: false, error: "Error occured while performing this operation"}) + + mock: (cmd, data) -> + if cmd == 'is_student_calibrated' + # change to test each version + response = + success: true + calibrated: false + else if cmd == 'show_calibration_essay' + response = + success: true + submission_id: 1 + submission_key: 'abcd' + student_response: 'I am a fake response' + prompt: 'Answer this question' + rubric: 'This is a rubric.' + max_score: 4 + + + return response + class PeerGradingProblem constructor: (backend) -> + @prompt_wrapper = $('.prompt-wrapper') + @backend = backend + + # ugly hack to prevent this code from trying to run on the + # general peer grading page + if( @prompt_wrapper.length == 0) + return + + # get the location of the problem + @location = $('.peer-grading').data('location') + + # get the other elements we want to fill in + @submission_container = $('.submission-container') + @prompt_container = $('.prompt-container') + @rubric_container = $('.rubric-container') + @error_container = $('.error-container') - @render_problem() - - render_problem: () -> - # do this when it makes sense - @error_container.toggle(not @error_container.is(':empty')) + @is_calibrated_check() -backend = {} + is_calibrated_check: () => + @backend.post('is_student_calibrated', {}, @calibration_check_callback) + + + fetch_calibration_essay: ()=> + @backend.post('show_calibration_essay', {location: @location}, @render_calibration) + + render_calibration: (response) => + if response.success + #TODO: fill this in + + @submission_container.html("

Calibration Essay

") + @submission_container.append(response.student_response) + @prompt_container.html(response.prompt) + @rubric_container.html(response.rubric) + + else + @error_container.show() + @error_container.html(response.error) + + render_submission: (response) -> + #TODO: fill this in + + calibration_check_callback: (response) => + if response.success + # check whether or not we're still calibrating + if response.calibrated + @fetch_submission() + @calibration = false + else + @fetch_calibration_essay() + @calibration = true + + + + +mock_backend = true +ajax_url = $('.peer-grading').data('ajax_url') +backend = new PeerGradingProblemBackend(ajax_url, mock_backend) $(document).ready(() -> new PeerGradingProblem(backend)) diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index b5a1408fde..02c69954b7 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -19,7 +19,7 @@
${error_text}

Peer Grading

Instructions

-

+

Here are a list of problems that need to be peer graded for this course.

% if success:
    %for problem in problem_list: diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 7ea47f04f7..9e6e44e699 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -15,8 +15,58 @@ <%static:js group='peer_grading'/> +
    -
    -
    ${error_text}
    +
    +
    +
    + +
    +
    +

    Calibration

    +
    +
    +

    Grading

    +
    +
    + +
    +
    +

    Question

    +
    +
    +
    +
    +

    Grading Rubric

    +
    +
    +
    + +
    + + +
    +

    Grading

    + +
    +
    +

    +
    +
    +
    +
    +

    +

    + +
    + + +
    + +
    + +
    +
    diff --git a/lms/urls.py b/lms/urls.py index 7f30224ad5..4e4fafcc23 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -250,12 +250,17 @@ if settings.COURSEWARE_ENABLED: 'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$', 'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$', - 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), + + + # Peer Grading url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading$', 'open_ended_grading.views.peer_grading', name='peer_grading'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/problem/(?P.*)$', 'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$', + 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$', + 'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'), ) From bd47b0c79ae6778a11a4bb9cca98312235432026 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 12:59:34 -0500 Subject: [PATCH 008/329] New urls and corresponding views in the grading service --- .../peer_grading_service.py | 85 +++++++++++++++++-- lms/urls.py | 7 +- 2 files changed, 86 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 daccdd53af..cd4a94f26d 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -118,9 +118,6 @@ def get_next_submission(request, course_id): mimetype="application/json") def _get_next_submission(course_id, grader_id, location): - """ - Implementation of get_next (also called from save_grade) -- returns a json string - """ try: return peer_grading_service().get_next_submission(location, grader_id) except GradingServiceError: @@ -129,6 +126,57 @@ def _get_next_submission(course_id, grader_id, location): return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) +def save_grade(request, course_id): + """ + TODO: fill in this documentation + """ + _check_post(request) + required = set(['location', 'grader_id', 'submission_id', 'submission_key', 'score', 'feedback']) + success, message = _check_required(request, required) + if not success: + return _err_response(message) + grader_id = request.user.id + p = request.POST + location = p['location'] + submission_id = p['submission_id'] + score = p['score'] + feedback = p['feedback'] + submission_key = p['submission_key'] + try: + response = peer_grading_service().save_grade(grader_id, submission_id, + score, feedback, submission_key) + return HttpResponse(response, mimetype="application/json") + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(staff_grading_service().url)) + return json.dumps({'success': False, + 'error': 'Could not connect to grading service'}) + + + +def is_student_calibrated(request, course_id): + """ + TODO: fill in this documentation + """ + _check_post(request) + required = set(['location']) + success, message = _check_required(request, required) + if not success: + return _err_response(message) + grader_id = request.user.id + p = request.POST + location = p['location'] + + try: + response = peer_grading_service().is_student_calibrated(location, grader_id) + return HttpResponse(response, mimetype="application/json") + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(staff_grading_service().url)) + return json.dumps({'success': False, + 'error': 'Could not connect to grading service'}) + + def show_calibration_essay(request, course_id): """ @@ -144,12 +192,39 @@ def show_calibration_essay(request, course_id): grader_id = request.user.id p = request.POST location = p['location'] + return HttpResponse(_next_calibration_essay(course_id, grader_id, location), + mimetype="application/json") + +def _next_calibration_essay(course_id, grader_id, location): try: - response = peer_grading_service().show_calibration_essay(location, grader_id) - return HttpResponse(response, mimetype="application/json") + return peer_grading_service().show_calibration_essay(location, grader_id) except GradingServiceError: log.exception("Error from grading service. server url: {0}" .format(staff_grading_service().url)) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) + +def save_calibration_essay(request, course_id): + """ + """ + _check_post(request) + + required = set(['location', 'calibration_essay_id', 'submission_key', 'score', 'feedback']) + success, message = _check_required(request, required) + if not success: + return _err_response(message) + grader_id = request.user.id + p = request.POST + location = p['location'] + calibration_essay_id = p['calibration_essay_id'] + submission_key = p['submission_key'] + score = p['score'] + feedback = p['feedback'] + + try: + response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, submission_key, score, feedback) + return HttpResponse(response, mimetype="application/json") + except GradingServiceError: + log.exception("Error saving calibration grade") + return _err_response('Could not connect to grading service') diff --git a/lms/urls.py b/lms/urls.py index 4e4fafcc23..ed12aa9682 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -261,7 +261,12 @@ if settings.COURSEWARE_ENABLED: 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$', 'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'), - + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/is_student_calibrated$', + 'open_ended_grading.peer_grading_service.is_student_calibrated', name='peer_grading_is_student_calibrated'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/save_grade$', + 'open_ended_grading.peer_grading_service.save_grade', name='peer_grading_save_grade'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/save_calibration_essay$', + 'open_ended_grading.peer_grading_service.save_calibration_essay', name='peer_grading_save_calibration_essay'), ) # discussion forums live within courseware, so courseware must be enabled first From 962d83d71b81f20a85c122ee83afeeb233a989a6 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 13:00:02 -0500 Subject: [PATCH 009/329] Display a message when there are no problems to grade --- lms/templates/peer_grading/peer_grading.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 02c69954b7..ee338cecbb 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -21,13 +21,19 @@

    Instructions

    Here are a list of problems that need to be peer graded for this course.

    % if success: - + % if len(problem_list) == 0: +
    + Nothing to grade! +
    + %else: + + %endif %endif
From 1c1034c63ba8ba75239e3084fbaa97107bd76f51 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 13:47:03 -0500 Subject: [PATCH 010/329] Pass location as a parameter, not as a part of the url --- lms/djangoapps/open_ended_grading/views.py | 7 ++++++- lms/urls.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 6d41d47812..1026888987 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -71,6 +71,7 @@ def peer_grading(request, course_id): # call problem list service success = False error_text = "" + problem_list = [] try: problem_list_text = peer_gs.get_problem_list(course_id, request.user.id) problem_list_json = json.loads(problem_list_text) @@ -83,6 +84,9 @@ def peer_grading(request, course_id): except GradingServiceError: error_text = "Error occured while contacting the grading service" success = False + except ValueError: + error_text = "Could not get problem list" + success = False ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) if not ajax_url.endswith('/'): @@ -100,11 +104,12 @@ def peer_grading(request, course_id): 'staff_access': False, }) -def peer_grading_problem(request, course_id, problem_location): +def peer_grading_problem(request, course_id): ''' Show individual problem interface ''' course = get_course_with_access(request.user, course_id, 'load') + problem_location = request.GET.get("location") ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) if not ajax_url.endswith('/'): diff --git a/lms/urls.py b/lms/urls.py index ed12aa9682..5bc55d6f10 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -255,7 +255,7 @@ if settings.COURSEWARE_ENABLED: # Peer Grading url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading$', 'open_ended_grading.views.peer_grading', name='peer_grading'), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/problem/(?P.*)$', + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/problem$', 'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$', 'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'), From c0001597a3f8f2ade4c183c252648e436788fee1 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 13:47:29 -0500 Subject: [PATCH 011/329] Update some of the templates so that they don't break --- lms/templates/peer_grading/peer_grading.html | 2 +- lms/templates/peer_grading/peer_grading_problem.html | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index ee338cecbb..484bb94182 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -29,7 +29,7 @@ diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 9e6e44e699..116da94ece 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -17,7 +17,7 @@
-
+
@@ -53,6 +53,8 @@

+ +

From becffd4dbb5a75410309fe6da0d9181de2493dff Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 14:08:56 -0500 Subject: [PATCH 012/329] Updated html and javascript for new pages as well as a fix for the peer grading service --- .../peer_grading_service.py | 1 + .../peer_grading/peer_grading_problem.coffee | 146 ++++++++++++++++-- .../peer_grading/peer_grading_problem.html | 6 +- 3 files changed, 135 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index cd4a94f26d..70d0721b3b 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -65,6 +65,7 @@ class PeerGradingService(GradingService): return response +_service = None def peer_grading_service(): """ Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True, 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 5b7aef18c7..7c0921cc98 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -18,14 +18,27 @@ class PeerGradingProblemBackend success: true calibrated: false else if cmd == 'show_calibration_essay' + #response = + # success: false + # error: "There was an error" response = success: true submission_id: 1 submission_key: 'abcd' - student_response: 'I am a fake response' + student_response: 'I am a fake calibration response' prompt: 'Answer this question' rubric: 'This is a rubric.' max_score: 4 + else if cmd == 'get_next_submission' + response = + success: true + submission_id: 1 + submission_key: 'abcd' + student_response: 'I am a fake student response' + prompt: 'Answer this question' + rubric: 'This is a rubric.' + max_score: 4 + return response @@ -48,35 +61,51 @@ class PeerGradingProblem @submission_container = $('.submission-container') @prompt_container = $('.prompt-container') @rubric_container = $('.rubric-container') + @instructions_panel = $('.instructions-panel') + @content_panel = $('.content-panel') @error_container = $('.error-container') + @submission_key_input = $("input[name='submission-key']") + @essay_id_input = $("input[name='essay-id']") + + @score_selection_container = $('.score-selection-container') + @score = null + + @submit_button = $('.submit-button') + @action_button = $('.action-button') + + @action_button.click -> document.location.reload(true) + @is_calibrated_check() + ########## + # + # Ajax calls to the backend + # + ########## is_calibrated_check: () => - @backend.post('is_student_calibrated', {}, @calibration_check_callback) + @backend.post('is_student_calibrated', {location: @location}, @calibration_check_callback) - - fetch_calibration_essay: ()=> + fetch_calibration_essay: () => @backend.post('show_calibration_essay', {location: @location}, @render_calibration) - render_calibration: (response) => - if response.success - #TODO: fill this in + fetch_submission_essay: () => + @backend.post('get_next_submission', {location: @location}, @render_submission) - @submission_container.html("

Calibration Essay

") - @submission_container.append(response.student_response) - @prompt_container.html(response.prompt) - @rubric_container.html(response.rubric) + submit_calibration_essay: ()-> + #TODO: onclick of the submit button. submits the calibration essay grade - else - @error_container.show() - @error_container.html(response.error) - - render_submission: (response) -> - #TODO: fill this in + submit_grade: () -> + #TODO: onclick of the submit button. submits the grade + + ########## + # + # Callbacks for various events + # + ########## calibration_check_callback: (response) => if response.success # check whether or not we're still calibrating @@ -86,6 +115,89 @@ class PeerGradingProblem else @fetch_calibration_essay() @calibration = true + else if response.error + @render_error(response.error) + else + @render_error("Error contacting the grading service") + + + submission_callback: (response) => + if response.success + @is_calibrated_check() + else + if response.error + @render_error(response.error) + else + @render_error("Error occurred while submitting grade") + + graded_callback: (event) => + @score = event.target.value + @show_submit_button() + + + + ########## + # + # Rendering methods and helpers + # + ########## + render_calibration: (response) => + if response.success + + # load in all the data + @submission_container.html("

Calibration Essay

") + @render_submission_data(response) + + # TODO: indicate that we're in calibration mode + + else + if response.error + @render_error(response.error) + else + @render_error("An error occurred while contacting the grading server") + + render_submission_data: (response) => + @content_panel.show() + @submission_container.append(response.student_response) + @prompt_container.html(response.prompt) + @rubric_container.html(response.rubric) + @submission_key_input.val(response.submission_key) + @essay_id_input.val(response.submission_id) + @setup_score_selection(response.max_score) + @submit_button.hide() + @action_button.hide() + + + render_submission: (response) => + #TODO: fill this in + @submit_button.hide() + @render_submission_data(response) + + render_error: (error_message) => + @error_container.show() + @error_container.html(error_message) + @content_panel.hide() + + show_submit_button: () => + @submit_button.show() + + setup_score_selection: (max_score) => + # first, get rid of all the old inputs, if any. + @score_selection_container.html('Choose score: ') + + # Now create new labels and inputs for each possible score. + for score in [0..max_score] + id = 'score-' + score + label = """""" + + input = """ + + """ # " fix broken parsing in emacs + @score_selection_container.append(input + label) + + # And now hook up an event handler again + $("input[name='score-selection']").change @graded_callback + diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 116da94ece..dd577c1295 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -21,6 +21,7 @@
+

Calibration

@@ -68,7 +69,10 @@
-
+
+
+
+ From 078b2a5b95e742c49108a0c01abbb395fbb4d129 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 16:19:04 -0500 Subject: [PATCH 013/329] Fix up some minor state issues and complete basic JS functionality for page --- .../peer_grading_service.py | 6 +- .../peer_grading/peer_grading_problem.coffee | 88 +++++++++++++++---- lms/static/sass/course/_staff_grading.scss | 5 ++ 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 70d0721b3b..af04b0c43d 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -132,7 +132,7 @@ def save_grade(request, course_id): TODO: fill in this documentation """ _check_post(request) - required = set(['location', 'grader_id', 'submission_id', 'submission_key', 'score', 'feedback']) + required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback']) success, message = _check_required(request, required) if not success: return _err_response(message) @@ -211,14 +211,14 @@ def save_calibration_essay(request, course_id): """ _check_post(request) - required = set(['location', 'calibration_essay_id', 'submission_key', 'score', 'feedback']) + required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback']) success, message = _check_required(request, required) if not success: return _err_response(message) grader_id = request.user.id p = request.POST location = p['location'] - calibration_essay_id = p['calibration_essay_id'] + calibration_essay_id = p['submission_id'] submission_key = p['submission_key'] score = p['score'] feedback = p['feedback'] 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 7c0921cc98..20a7948aa2 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -16,7 +16,7 @@ class PeerGradingProblemBackend # change to test each version response = success: true - calibrated: false + calibrated: true else if cmd == 'show_calibration_essay' #response = # success: false @@ -38,7 +38,12 @@ class PeerGradingProblemBackend prompt: 'Answer this question' rubric: 'This is a rubric.' max_score: 4 - + else if cmd == 'save_calibration_essay' + response = + success: true + else if cmd == 'save_grade' + response = + success: true return response @@ -61,13 +66,15 @@ class PeerGradingProblem @submission_container = $('.submission-container') @prompt_container = $('.prompt-container') @rubric_container = $('.rubric-container') - @instructions_panel = $('.instructions-panel') + @calibration_panel = $('.calibration-panel') + @grading_panel = $('.grading-panel') @content_panel = $('.content-panel') @error_container = $('.error-container') @submission_key_input = $("input[name='submission-key']") @essay_id_input = $("input[name='essay-id']") + @feedback_area = $('.feedback-area') @score_selection_container = $('.score-selection-container') @score = null @@ -94,11 +101,23 @@ class PeerGradingProblem fetch_submission_essay: () => @backend.post('get_next_submission', {location: @location}, @render_submission) - submit_calibration_essay: ()-> - #TODO: onclick of the submit button. submits the calibration essay grade + construct_data: () => + data = + score: @score + location: @location + submission_id: @essay_id_input.val() + submission_key: @submission_key_input.val() + feedback: @feedback_area.val() + return data - submit_grade: () -> - #TODO: onclick of the submit button. submits the grade + + submit_calibration_essay: ()=> + data = @construct_data() + @backend.post('save_calibration_essay', data, @submission_callback) + + submit_grade: () => + data = @construct_data() + @backend.post('save_grade', data, @submission_callback) ########## @@ -110,7 +129,7 @@ class PeerGradingProblem if response.success # check whether or not we're still calibrating if response.calibrated - @fetch_submission() + @fetch_submission_essay() @calibration = false else @fetch_calibration_essay() @@ -147,18 +166,55 @@ class PeerGradingProblem # load in all the data @submission_container.html("

Calibration Essay

") @render_submission_data(response) - # TODO: indicate that we're in calibration mode + @calibration_panel.addClass('current-state') + @grading_panel.removeClass('current-state') + # clear out all of the existing text + @calibration_panel.find('p').remove() + @grading_panel.find('p').remove() + + # add in new text + + + @submit_button.click @submit_calibration_essay + + else if response.error + @render_error(response.error) else - if response.error - @render_error(response.error) - else - @render_error("An error occurred while contacting the grading server") + @render_error("An error occurred while retrieving the next calibration essay") + + render_submission: (response) => + if response.success + #TODO: fill this in + @submit_button.hide() + @submission_container.html("

Submitted Essay

") + @render_submission_data(response) + + @calibration_panel.removeClass('current-state') + @grading_panel.addClass('current-state') + + # clear out all of the existing text + @calibration_panel.find('p').remove() + @grading_panel.find('p').remove() + + @submit_button.click @submit_grade + else if response.error + @render_error(response.error) + else + @render_error("An error occured when retrieving the next submission.") + + + make_paragraphs: (text) -> + paragraph_split = text.split(/\n\s*\n/) + new_text = '' + for paragraph in paragraph_split + new_text += "

#{paragraph}

" + return new_text render_submission_data: (response) => @content_panel.show() - @submission_container.append(response.student_response) + @submission_container.append(@make_paragraphs(response.student_response)) @prompt_container.html(response.prompt) @rubric_container.html(response.rubric) @submission_key_input.val(response.submission_key) @@ -168,10 +224,6 @@ class PeerGradingProblem @action_button.hide() - render_submission: (response) => - #TODO: fill this in - @submit_button.hide() - @render_submission_data(response) render_error: (error_message) => @error_container.show() diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index bb7f6cef45..d2006e10a7 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -82,6 +82,11 @@ div.peer-grading{ margin-bottom:5px; font-size: .8em; } + + .current-state + { + background: #eee; + } padding: 40px; } From fe86c25f7218182089c90d41a0857009957ec1b0 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 3 Jan 2013 17:23:30 -0500 Subject: [PATCH 014/329] Bug fixes for the JS and the peer grading service --- .../open_ended_grading/peer_grading_service.py | 10 +++++----- .../src/peer_grading/peer_grading_problem.coffee | 7 +++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index af04b0c43d..6b35f55f01 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -33,12 +33,13 @@ class PeerGradingService(GradingService): {'location': problem_location, 'grader_id': grader_id}) return response - def save_grade(self, grader_id, submission_id, score, feedback, submission_key): + def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key): data = {'grader_id' : grader_id, 'submission_id' : submission_id, 'score' : score, 'feedback' : feedback, - 'submission_key': submission_key} + 'submission_key': submission_key, + 'location': location} return self.post(self.save_grade_url, False, data) def is_student_calibrated(self, problem_location, grader_id): @@ -61,7 +62,6 @@ class PeerGradingService(GradingService): def get_problem_list(self, course_id, grader_id): params = {'course_id': course_id, 'student_id': grader_id} response = self.get(self.get_problem_list_url, False, params) - log.debug("Response! {0}".format(response)) return response @@ -115,7 +115,7 @@ def get_next_submission(request, course_id): p = request.POST location = p['location'] - return HttpResponse(_get_next(course_id, request.user.id, location), + return HttpResponse(_get_next_submission(course_id, request.user.id, location), mimetype="application/json") def _get_next_submission(course_id, grader_id, location): @@ -144,7 +144,7 @@ def save_grade(request, course_id): feedback = p['feedback'] submission_key = p['submission_key'] try: - response = peer_grading_service().save_grade(grader_id, submission_id, + response = peer_grading_service().save_grade(location, grader_id, submission_id, score, feedback, submission_key) return HttpResponse(response, mimetype="application/json") except GradingServiceError: 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 20a7948aa2..04b47671a1 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -45,7 +45,6 @@ class PeerGradingProblemBackend response = success: true - return response @@ -101,9 +100,9 @@ class PeerGradingProblem fetch_submission_essay: () => @backend.post('get_next_submission', {location: @location}, @render_submission) - construct_data: () => + construct_data: () -> data = - score: @score + score: $('input[name="score-selection"]:checked').val() location: @location submission_id: @essay_id_input.val() submission_key: @submission_key_input.val() @@ -254,7 +253,7 @@ class PeerGradingProblem -mock_backend = true +mock_backend = false ajax_url = $('.peer-grading').data('ajax_url') backend = new PeerGradingProblemBackend(ajax_url, mock_backend) $(document).ready(() -> new PeerGradingProblem(backend)) From b5f34a9da6d7d02c0964bd4a02b84de95dad8421 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 11:03:24 -0500 Subject: [PATCH 015/329] Show special text when calibrating or when grading. --- .../peer_grading/peer_grading_problem.coffee | 30 +++++++++++------ lms/static/sass/course/_staff_grading.scss | 33 +++++++++++++++++-- .../peer_grading/peer_grading_problem.html | 12 +++++++ 3 files changed, 63 insertions(+), 12 deletions(-) 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 04b47671a1..d269a23c1e 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -69,6 +69,8 @@ class PeerGradingProblem @grading_panel = $('.grading-panel') @content_panel = $('.content-panel') + @grading_wrapper =$('.grading-wrapper') + @error_container = $('.error-container') @submission_key_input = $("input[name='submission-key']") @@ -76,7 +78,6 @@ class PeerGradingProblem @feedback_area = $('.feedback-area') @score_selection_container = $('.score-selection-container') - @score = null @submit_button = $('.submit-button') @action_button = $('.action-button') @@ -138,6 +139,13 @@ class PeerGradingProblem else @render_error("Error contacting the grading service") + calibration_callback: (response) => + if response.success + # display correct grade + @grading_wrapper.hide() + + else if response.error + @render_error(response.error) submission_callback: (response) => if response.success @@ -149,7 +157,6 @@ class PeerGradingProblem @render_error("Error occurred while submitting grade") graded_callback: (event) => - @score = event.target.value @show_submit_button() @@ -170,12 +177,14 @@ class PeerGradingProblem @grading_panel.removeClass('current-state') # clear out all of the existing text - @calibration_panel.find('p').remove() - @grading_panel.find('p').remove() - - # add in new text + @calibration_panel.find('.calibration-text').show() + @grading_panel.find('.calibration-text').show() + @calibration_panel.find('.grading-text').hide() + @grading_panel.find('.grading-text').hide() + # TODO: add in new text + @submit_button.unbind('click') @submit_button.click @submit_calibration_essay else if response.error @@ -194,9 +203,12 @@ class PeerGradingProblem @grading_panel.addClass('current-state') # clear out all of the existing text - @calibration_panel.find('p').remove() - @grading_panel.find('p').remove() + @calibration_panel.find('.calibration-text').hide() + @grading_panel.find('.calibration-text').hide() + @calibration_panel.find('.grading-text').show() + @grading_panel.find('.grading-text').show() + @submit_button.unbind('click') @submit_button.click @submit_grade else if response.error @render_error(response.error) @@ -251,8 +263,6 @@ class PeerGradingProblem - - mock_backend = false ajax_url = $('.peer-grading').data('ajax_url') backend = new PeerGradingProblemBackend(ajax_url, mock_backend) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index d2006e10a7..2ca355e304 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -83,10 +83,39 @@ div.peer-grading{ font-size: .8em; } - .current-state + .instructions-panel { - background: #eee; + + > div + { + padding: 10px; + margin: 0px; + border: 1px solid black; + } + .calibration-panel + { + float:left; + width:47%; + } + .grading-panel + { + float:right; + width: 47%; + } + .current-state + { + background: #eee; + } + &:after + { + content:"."; + display:block; + height:0; + visibility: hidden; + clear:both; + } } + padding: 40px; } diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index dd577c1295..c29d509bb8 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -25,9 +25,21 @@

Calibration

+
+

You are being calibrated on this problem

+
+
+

You have successfully calibrated on this problem

+

Grading

+
+

You cannot start grading until you have finished calibrating

+
+
+

Now that you are done calibrating, you can now start grading.

+
From ad63c492bedc75e6dab86bb3de1c117315805fd1 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 12:42:38 -0500 Subject: [PATCH 016/329] Minor updates to the page to handle showing Calibration feedback. Also, allow for the hiding of the Prompt and the Rubric. --- lms/envs/common.py | 1 + .../peer_grading/peer_grading_problem.coffee | 17 ++++++++++++++--- lms/static/sass/course/_staff_grading.scss | 5 ++++- .../peer_grading/peer_grading_problem.html | 8 +++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index a24422df50..3b83b708aa 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -497,6 +497,7 @@ PIPELINE_JS = { for pth in sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee'))\ if (pth not in courseware_only_js and pth not in discussion_js and + pth not in peer_grading_js and pth not in staff_grading_js) ] + [ 'js/form.ext.js', 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 d269a23c1e..6437742aac 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -16,7 +16,7 @@ class PeerGradingProblemBackend # change to test each version response = success: true - calibrated: true + calibrated: false else if cmd == 'show_calibration_essay' #response = # success: false @@ -41,6 +41,7 @@ class PeerGradingProblemBackend else if cmd == 'save_calibration_essay' response = success: true + correct_score: 2 else if cmd == 'save_grade' response = success: true @@ -70,6 +71,7 @@ class PeerGradingProblem @content_panel = $('.content-panel') @grading_wrapper =$('.grading-wrapper') + @calibration_feedback_panel = $('.calibration-feedback') @error_container = $('.error-container') @@ -81,8 +83,14 @@ class PeerGradingProblem @submit_button = $('.submit-button') @action_button = $('.action-button') + @calibration_feedback_button = $('.calibration-feedback-button') + Collapsible.setCollapsibles(@content_panel) @action_button.click -> document.location.reload(true) + @calibration_feedback_button.click => + @calibration_feedback_panel.hide() + @grading_wrapper.show() + @is_calibrated_check @is_calibrated_check() @@ -113,7 +121,7 @@ class PeerGradingProblem submit_calibration_essay: ()=> data = @construct_data() - @backend.post('save_calibration_essay', data, @submission_callback) + @backend.post('save_calibration_essay', data, @calibration_callback) submit_grade: () => data = @construct_data() @@ -143,6 +151,8 @@ class PeerGradingProblem if response.success # display correct grade @grading_wrapper.hide() + @calibration_feedback_panel.show() + @calibration_feedback_panel.prepend("

The correct grade is: #{response.correct_score}

") else if response.error @render_error(response.error) @@ -233,6 +243,7 @@ class PeerGradingProblem @setup_score_selection(response.max_score) @submit_button.hide() @action_button.hide() + @calibration_feedback_panel.hide() @@ -263,7 +274,7 @@ class PeerGradingProblem -mock_backend = false +mock_backend = true ajax_url = $('.peer-grading').data('ajax_url') backend = new PeerGradingProblemBackend(ajax_url, mock_backend) $(document).ready(() -> new PeerGradingProblem(backend)) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 2ca355e304..06a83651d7 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -37,7 +37,6 @@ div.peer-grading{ } .prompt-information-container, - .submission-wrapper, .rubric-wrapper, .grading-container { @@ -50,6 +49,10 @@ div.peer-grading{ padding: 15px; margin-left: 0px; } + .submission-wrapper + { + padding: 15px; + } .meta-info-wrapper { background-color: #eee; diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index c29d509bb8..08fd33be66 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -43,7 +43,11 @@ -
+
+
+ Display problem information +
+

Question

@@ -56,6 +60,7 @@
+
@@ -85,6 +90,7 @@
+
From befe36f6263dcd793c4d4b54d32469714f754c01 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 14:11:48 -0500 Subject: [PATCH 017/329] Improvements to calibration feedback step --- .../peer_grading/peer_grading_problem.coffee | 49 +++++++++++++++---- lms/static/sass/course/_staff_grading.scss | 20 ++++++++ .../peer_grading/peer_grading_problem.html | 9 +++- 3 files changed, 66 insertions(+), 12 deletions(-) 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 6437742aac..4463c1ca85 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -25,9 +25,26 @@ class PeerGradingProblemBackend success: true submission_id: 1 submission_key: 'abcd' - student_response: 'I am a fake calibration response' - prompt: 'Answer this question' - rubric: 'This is a rubric.' + student_response: ''' + Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + ''' + prompt: ''' +

S11E3: Metal Bands

+

Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.

+

* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?

+

This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.

+ ''' + rubric: ''' +
    +
  • Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.
  • +
  • Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.
  • +
  • Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.
  • +
+ +

Please score your response according to how many of the above components you identified:

+ ''' max_score: 4 else if cmd == 'get_next_submission' response = @@ -80,6 +97,7 @@ class PeerGradingProblem @feedback_area = $('.feedback-area') @score_selection_container = $('.score-selection-container') + @score = null @submit_button = $('.submit-button') @action_button = $('.action-button') @@ -90,7 +108,7 @@ class PeerGradingProblem @calibration_feedback_button.click => @calibration_feedback_panel.hide() @grading_wrapper.show() - @is_calibrated_check + @is_calibrated_check() @is_calibrated_check() @@ -111,7 +129,7 @@ class PeerGradingProblem construct_data: () -> data = - score: $('input[name="score-selection"]:checked').val() + score: @score location: @location submission_id: @essay_id_input.val() submission_key: @submission_key_input.val() @@ -149,11 +167,7 @@ class PeerGradingProblem calibration_callback: (response) => if response.success - # display correct grade - @grading_wrapper.hide() - @calibration_feedback_panel.show() - @calibration_feedback_panel.prepend("

The correct grade is: #{response.correct_score}

") - + @render_calibration_feedback(response) else if response.error @render_error(response.error) @@ -167,6 +181,7 @@ class PeerGradingProblem @render_error("Error occurred while submitting grade") graded_callback: (event) => + @score = event.target.value @show_submit_button() @@ -246,6 +261,20 @@ class PeerGradingProblem @calibration_feedback_panel.hide() + render_calibration_feedback: (response) => + # display correct grade + #@grading_wrapper.hide() + @calibration_feedback_panel.show() + calibration_wrapper = $('.calibration-feedback-wrapper') + calibration_wrapper.html("

The score you gave was: #{@score}. The correct score is: #{response.correct_score}

") + score = parseInt(@score) + correct_score = parseInt(response.correct_score) + + if score == correct_score + calibration_wrapper.append("

Congratulations! Your score matches the correct one!

") + else + calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") + render_error: (error_message) => @error_container.show() diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 06a83651d7..39868ecd22 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -38,6 +38,7 @@ div.peer-grading{ .prompt-information-container, .rubric-wrapper, + .calibration-feedback-wrapper, .grading-container { border: 1px solid gray; @@ -51,6 +52,14 @@ div.peer-grading{ } .submission-wrapper { + h3 + { + margin-bottom: 15px; + } + p + { + margin-left:10px; + } padding: 15px; } .meta-info-wrapper @@ -119,6 +128,17 @@ div.peer-grading{ } } + + .collapsible + { + margin-left: 0px; + header + { + margin-top:20px; + margin-bottom:20px; + font-size: 1.2em; + } + } padding: 40px; } diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 08fd33be66..31e8efe290 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -45,7 +45,7 @@
- Display problem information + Show Problem Details
@@ -90,7 +90,12 @@
-
+
+

How did I do?

+
+
+ +
From fa09e25a1a381333cf3eb9059c2bb40a782f007b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 15:18:39 -0500 Subject: [PATCH 018/329] Add interstitial page between calibration and grading. Make individual Prompt and Rubric Sections hideable. --- .../peer_grading/peer_grading_problem.coffee | 47 +++++++++++++------ lms/static/sass/course/_staff_grading.scss | 9 +++- .../peer_grading/peer_grading_problem.html | 26 ++++++---- 3 files changed, 55 insertions(+), 27 deletions(-) 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 4463c1ca85..a341b55238 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -2,6 +2,7 @@ class PeerGradingProblemBackend constructor: (ajax_url, mock_backend) -> @mock_backend = mock_backend @ajax_url = ajax_url + @mock_cnt = 0 post: (cmd, data, callback) -> if @mock_backend @@ -16,11 +17,12 @@ class PeerGradingProblemBackend # change to test each version response = success: true - calibrated: false + calibrated: @mock_cnt >= 2 else if cmd == 'show_calibration_essay' #response = # success: false # error: "There was an error" + @mock_cnt++ response = success: true submission_id: 1 @@ -58,7 +60,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t else if cmd == 'save_calibration_essay' response = success: true - correct_score: 2 + actual_score: 2 else if cmd == 'save_grade' response = success: true @@ -89,6 +91,8 @@ class PeerGradingProblem @grading_wrapper =$('.grading-wrapper') @calibration_feedback_panel = $('.calibration-feedback') + @interstitial_page = $('.interstitial-page') + @interstitial_page.hide() @error_container = $('.error-container') @@ -98,10 +102,12 @@ class PeerGradingProblem @score_selection_container = $('.score-selection-container') @score = null + @calibration = null @submit_button = $('.submit-button') @action_button = $('.action-button') @calibration_feedback_button = $('.calibration-feedback-button') + @interstitial_page_button = $('.interstitial-page-button') Collapsible.setCollapsibles(@content_panel) @action_button.click -> document.location.reload(true) @@ -110,6 +116,10 @@ class PeerGradingProblem @grading_wrapper.show() @is_calibrated_check() + @interstitial_page_button.click => + @interstitial_page.hide() + @is_calibrated_check() + @is_calibrated_check() @@ -154,12 +164,15 @@ class PeerGradingProblem calibration_check_callback: (response) => if response.success # check whether or not we're still calibrating - if response.calibrated - @fetch_submission_essay() + if response.calibrated and (@calibration == null or @calibration == false) @calibration = false + @fetch_submission_essay() + else if response.calibrated and @calibration == true + @calibration = false + @render_interstitial_page() else - @fetch_calibration_essay() @calibration = true + @fetch_calibration_essay() else if response.error @render_error(response.error) else @@ -262,20 +275,24 @@ class PeerGradingProblem render_calibration_feedback: (response) => - # display correct grade + # display correct grade #@grading_wrapper.hide() - @calibration_feedback_panel.show() - calibration_wrapper = $('.calibration-feedback-wrapper') - calibration_wrapper.html("

The score you gave was: #{@score}. The correct score is: #{response.correct_score}

") - score = parseInt(@score) - correct_score = parseInt(response.correct_score) + @calibration_feedback_panel.show() + calibration_wrapper = $('.calibration-feedback-wrapper') + calibration_wrapper.html("

The score you gave was: #{@score}. The actual score is: #{response.actual_score}

") + score = parseInt(@score) + actual_score = parseInt(response.actual_score) + #TODO: maybe do another variation depending on whether or not students are close to correct - if score == correct_score - calibration_wrapper.append("

Congratulations! Your score matches the correct one!

") - else - calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") + if score == actual_score + calibration_wrapper.append("

Congratulations! Your score matches the actual one!

") + else + calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") + render_interstitial_page: () => + @content_panel.hide() + @interstitial_page.show() render_error: (error_message) => @error_container.show() @error_container.html(error_message) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 39868ecd22..075db8dbdc 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -36,8 +36,8 @@ div.peer-grading{ } } - .prompt-information-container, - .rubric-wrapper, + .prompt-container, + .rubric-container, .calibration-feedback-wrapper, .grading-container { @@ -103,6 +103,11 @@ div.peer-grading{ padding: 10px; margin: 0px; border: 1px solid black; + h3 + { + text-align:center; + text-transform:uppercase; + } } .calibration-panel { diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 31e8efe290..cd01ddcfa2 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -43,23 +43,22 @@ -
-
- Show Problem Details -
-
-
-

Question

+
+ -
-

Grading Rubric

+
+
View Rubric
+
+
-
@@ -96,6 +95,13 @@ + +
+

Congratulations!

+

You have now completed calibration. You are now ready to start grading.

+ +
+ From 093560e85963e96636d1cd4bed074360ce0f2d32 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 16:36:28 -0500 Subject: [PATCH 019/329] Updates to copy and styling and a few bugfixes --- .../peer_grading/peer_grading_problem.coffee | 21 ++++++++++++++---- lms/static/sass/course/_staff_grading.scss | 22 +++++++++++++++---- .../peer_grading/peer_grading_problem.html | 17 +++++++------- 3 files changed, 44 insertions(+), 16 deletions(-) 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 a341b55238..43cd38e20d 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -54,8 +54,21 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t submission_id: 1 submission_key: 'abcd' student_response: 'I am a fake student response' - prompt: 'Answer this question' - rubric: 'This is a rubric.' + prompt: ''' +

S11E3: Metal Bands

+

Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.

+

* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?

+

This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.

+ ''' + rubric: ''' +
    +
  • Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.
  • +
  • Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.
  • +
  • Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.
  • +
+ +

Please score your response according to how many of the above components you identified:

+ ''' max_score: 4 else if cmd == 'save_calibration_essay' response = @@ -208,7 +221,7 @@ class PeerGradingProblem if response.success # load in all the data - @submission_container.html("

Calibration Essay

") + @submission_container.html("

Training Essay

") @render_submission_data(response) # TODO: indicate that we're in calibration mode @calibration_panel.addClass('current-state') @@ -288,7 +301,7 @@ class PeerGradingProblem calibration_wrapper.append("

Congratulations! Your score matches the actual one!

") else calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") - + @submit_button.hide() render_interstitial_page: () => @content_panel.hide() diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 075db8dbdc..47c437513b 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -36,8 +36,8 @@ div.peer-grading{ } } - .prompt-container, - .rubric-container, + .prompt-information-container, + .rubric-wrapper, .calibration-feedback-wrapper, .grading-container { @@ -102,11 +102,17 @@ div.peer-grading{ { padding: 10px; margin: 0px; - border: 1px solid black; + background: #eee; + height: 10em; h3 { text-align:center; text-transform:uppercase; + color: #777; + } + p + { + color: #777; } } .calibration-panel @@ -121,7 +127,11 @@ div.peer-grading{ } .current-state { - background: #eee; + background: #0F6B8A; + h3, p + { + color: white; + } } &:after { @@ -145,5 +155,9 @@ div.peer-grading{ } } + .interstitial-grading + { + } padding: 40px; } + diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index cd01ddcfa2..40c0146535 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -22,37 +22,38 @@
+

Peer Grading

-

Calibration

+

Learning to Grade

-

You are being calibrated on this problem

+

Before you can do any proper peer grading, you first need to understand how your own grading compares to that of the instrutor. Once your grades begin to match the instructor's, you will move on to grading your peers!

-

You have successfully calibrated on this problem

+

You have successfully managed to calibrate your answers to that of the instructors and have moved onto the next step in the peer grading process.

Grading

-

You cannot start grading until you have finished calibrating

+

You cannot start grading until you have graded a sufficient number of training problems and have been able to demonstrate that your scores closely match that of the instructor.

-

Now that you are done calibrating, you can now start grading.

+

Now that you have finished your training, you are now allowed to grade your peers. Please keep in mind that students are allowed to respond to the grades and feedback they receive.

-
View Rubric
+
Rubric
@@ -98,7 +99,7 @@

Congratulations!

-

You have now completed calibration. You are now ready to start grading.

+

You have now completed the calibration step. You are now ready to start grading.

From e410703e33d66caafc6cde68bcc4d24497491e04 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 4 Jan 2013 17:20:03 -0500 Subject: [PATCH 020/329] Minor bug fixes and improvements --- .../peer_grading/peer_grading_problem.coffee | 35 ++++++++++++++----- lms/static/sass/course/_staff_grading.scss | 4 ++- .../peer_grading/peer_grading_problem.html | 6 +++- 3 files changed, 34 insertions(+), 11 deletions(-) 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 43cd38e20d..0ffb5dc76d 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -53,7 +53,11 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t success: true submission_id: 1 submission_key: 'abcd' - student_response: 'I am a fake student response' + student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa. + +Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum. + +Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. ''' prompt: '''

S11E3: Metal Bands

Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.

@@ -101,6 +105,8 @@ class PeerGradingProblem @calibration_panel = $('.calibration-panel') @grading_panel = $('.grading-panel') @content_panel = $('.content-panel') + @grading_message = $('.grading-message') + @grading_message.hide() @grading_wrapper =$('.grading-wrapper') @calibration_feedback_panel = $('.calibration-feedback') @@ -176,10 +182,12 @@ class PeerGradingProblem ########## calibration_check_callback: (response) => if response.success - # check whether or not we're still calibrating + # if we haven't been calibrating before if response.calibrated and (@calibration == null or @calibration == false) @calibration = false @fetch_submission_essay() + # If we were calibrating before and no longer need to, + # show the interstitial page else if response.calibrated and @calibration == true @calibration = false @render_interstitial_page() @@ -200,6 +208,8 @@ class PeerGradingProblem submission_callback: (response) => if response.success @is_calibrated_check() + @grading_message.fadeIn() + @grading_message.html("

Grade sent successfully.

") else if response.error @render_error(response.error) @@ -207,6 +217,7 @@ class PeerGradingProblem @render_error("Error occurred while submitting grade") graded_callback: (event) => + @grading_message.hide() @score = event.target.value @show_submit_button() @@ -217,6 +228,7 @@ class PeerGradingProblem # Rendering methods and helpers # ########## + # renders a calibration essay render_calibration: (response) => if response.success @@ -227,13 +239,12 @@ class PeerGradingProblem @calibration_panel.addClass('current-state') @grading_panel.removeClass('current-state') - # clear out all of the existing text + # Display the right text @calibration_panel.find('.calibration-text').show() @grading_panel.find('.calibration-text').show() @calibration_panel.find('.grading-text').hide() @grading_panel.find('.grading-text').hide() - # TODO: add in new text @submit_button.unbind('click') @submit_button.click @submit_calibration_essay @@ -243,9 +254,9 @@ class PeerGradingProblem else @render_error("An error occurred while retrieving the next calibration essay") + # Renders a student submission to be graded render_submission: (response) => if response.success - #TODO: fill this in @submit_button.hide() @submission_container.html("

Submitted Essay

") @render_submission_data(response) @@ -253,7 +264,7 @@ class PeerGradingProblem @calibration_panel.removeClass('current-state') @grading_panel.addClass('current-state') - # clear out all of the existing text + # Display the correct text @calibration_panel.find('.calibration-text').hide() @grading_panel.find('.calibration-text').hide() @calibration_panel.find('.grading-text').show() @@ -276,12 +287,14 @@ class PeerGradingProblem render_submission_data: (response) => @content_panel.show() + @submission_container.append(@make_paragraphs(response.student_response)) @prompt_container.html(response.prompt) @rubric_container.html(response.rubric) @submission_key_input.val(response.submission_key) @essay_id_input.val(response.submission_id) @setup_score_selection(response.max_score) + @submit_button.hide() @action_button.hide() @calibration_feedback_panel.hide() @@ -290,17 +303,21 @@ class PeerGradingProblem render_calibration_feedback: (response) => # display correct grade #@grading_wrapper.hide() - @calibration_feedback_panel.show() + @calibration_feedback_panel.slideDown() calibration_wrapper = $('.calibration-feedback-wrapper') calibration_wrapper.html("

The score you gave was: #{@score}. The actual score is: #{response.actual_score}

") + + score = parseInt(@score) actual_score = parseInt(response.actual_score) - #TODO: maybe do another variation depending on whether or not students are close to correct if score == actual_score - calibration_wrapper.append("

Congratulations! Your score matches the actual one!

") + calibration_wrapper.append("

Congratulations! Your score matches the actual score!

") else calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") + + # disable score selection and submission from the grading interface + $("input[name='score-selection']").attr('disabled', true) @submit_button.hide() render_interstitial_page: () => diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 47c437513b..b99056a7f5 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -80,7 +80,8 @@ div.peer-grading{ } } } - .message-container + .message-container, + .grading-message { background-color: $yellow; padding: 10px; @@ -98,6 +99,7 @@ div.peer-grading{ .instructions-panel { + margin-right:20px; > div { padding: 10px; diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 40c0146535..9c11574f8d 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -19,7 +19,6 @@
-

Peer Grading

@@ -87,9 +86,13 @@
+
+
+ +

How did I do?

@@ -97,6 +100,7 @@
+

Congratulations!

You have now completed the calibration step. You are now ready to start grading.

From 2293a37f7d60eaeae7ba6bb06dc6c4a4f57696f6 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 7 Jan 2013 09:17:09 -0500 Subject: [PATCH 021/329] Move over old Staff Grading tests to new app. --- lms/djangoapps/instructor/tests.py | 93 ------------------ lms/djangoapps/open_ended_grading/tests.py | 104 +++++++++++++++++++-- 2 files changed, 98 insertions(+), 99 deletions(-) diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 865a97951e..2d17cee47d 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -25,7 +25,6 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \ FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT from django_comment_client.utils import has_forum_access -from instructor import staff_grading_service from courseware.access import _course_staff_group_name import courseware.tests.tests as ct from xmodule.modulestore.django import modulestore @@ -100,7 +99,6 @@ def action_name(operation, rolename): return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) -_mock_service = staff_grading_service.MockStaffGradingService() @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) class TestInstructorDashboardForumAdmin(ct.PageLoader): @@ -223,94 +221,3 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): self.assertTrue(response.content.find('{0}'.format(roles))>=0, 'not finding roles "{0}"'.format(roles)) -@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) -class TestStaffGradingService(ct.PageLoader): - ''' - Check that staff grading service proxy works. Basically just checking the - access control and error handling logic -- all the actual work is on the - backend. - ''' - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - self.student = 'view@test.com' - self.instructor = 'view2@test.com' - self.password = 'foo' - self.location = 'TestLocation' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.instructor, self.password) - self.activate_user(self.student) - self.activate_user(self.instructor) - - self.course_id = "edX/toy/2012_Fall" - self.toy = modulestore().get_course(self.course_id) - def make_instructor(course): - group_name = _course_staff_group_name(course.location) - g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) - - make_instructor(self.toy) - - self.mock_service = staff_grading_service.grading_service() - - self.logout() - - def test_access(self): - """ - Make sure only staff have access. - """ - self.login(self.student, self.password) - - # both get and post should return 404 - for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): - url = reverse(view_name, kwargs={'course_id': self.course_id}) - self.check_for_get_code(404, url) - self.check_for_post_code(404, url) - - - def test_get_next(self): - self.login(self.instructor, self.password) - - url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) - data = {'location': self.location} - - r = self.check_for_post_code(200, url, data) - d = json.loads(r.content) - self.assertTrue(d['success']) - self.assertEquals(d['submission_id'], self.mock_service.cnt) - self.assertIsNotNone(d['submission']) - self.assertIsNotNone(d['num_graded']) - self.assertIsNotNone(d['min_for_ml']) - self.assertIsNotNone(d['num_pending']) - self.assertIsNotNone(d['prompt']) - self.assertIsNotNone(d['ml_error_info']) - self.assertIsNotNone(d['max_score']) - self.assertIsNotNone(d['rubric']) - - - def test_save_grade(self): - self.login(self.instructor, self.password) - - url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) - - data = {'score': '12', - 'feedback': 'great!', - 'submission_id': '123', - 'location': self.location} - r = self.check_for_post_code(200, url, data) - d = json.loads(r.content) - self.assertTrue(d['success'], str(d)) - self.assertEquals(d['submission_id'], self.mock_service.cnt) - - def test_get_problem_list(self): - self.login(self.instructor, self.password) - - url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) - data = {} - - r = self.check_for_post_code(200, url, data) - d = json.loads(r.content) - self.assertTrue(d['success'], str(d)) - self.assertIsNotNone(d['problem_list']) - - diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 501deb776c..0636452779 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -6,11 +6,103 @@ Replace this with more appropriate tests for your application. """ from django.test import TestCase +from instructor import staff_grading_service +from django.core.urlresolvers import reverse +from django.contrib.auth.models import Group + +from courseware.access import _course_staff_group_name +import courseware.tests.tests as ct +from xmodule.modulestore.django import modulestore +import xmodule.modulestore.django + +_mock_service = staff_grading_service.MockStaffGradingService() + +@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) +class TestStaffGradingService(ct.PageLoader): + ''' + Check that staff grading service proxy works. Basically just checking the + access control and error handling logic -- all the actual work is on the + backend. + ''' + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.student = 'view@test.com' + self.instructor = 'view2@test.com' + self.password = 'foo' + self.location = 'TestLocation' + self.create_account('u1', self.student, self.password) + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.student) + self.activate_user(self.instructor) + + self.course_id = "edX/toy/2012_Fall" + self.toy = modulestore().get_course(self.course_id) + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(ct.user(self.instructor)) + + make_instructor(self.toy) + + self.mock_service = staff_grading_service.grading_service() + + self.logout() + + def test_access(self): + """ + Make sure only staff have access. + """ + self.login(self.student, self.password) + + # both get and post should return 404 + for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): + url = reverse(view_name, kwargs={'course_id': self.course_id}) + self.check_for_get_code(404, url) + self.check_for_post_code(404, url) -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) + def test_get_next(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) + data = {'location': self.location} + + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success']) + self.assertEquals(d['submission_id'], self.mock_service.cnt) + self.assertIsNotNone(d['submission']) + self.assertIsNotNone(d['num_graded']) + self.assertIsNotNone(d['min_for_ml']) + self.assertIsNotNone(d['num_pending']) + self.assertIsNotNone(d['prompt']) + self.assertIsNotNone(d['ml_error_info']) + self.assertIsNotNone(d['max_score']) + self.assertIsNotNone(d['rubric']) + + + def test_save_grade(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) + + data = {'score': '12', + 'feedback': 'great!', + 'submission_id': '123', + 'location': self.location} + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success'], str(d)) + self.assertEquals(d['submission_id'], self.mock_service.cnt) + + def test_get_problem_list(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) + data = {} + + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success'], str(d)) + self.assertIsNotNone(d['problem_list']) From e1ec4eec71061646771af5a5754487c7300cfa30 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 7 Jan 2013 14:46:32 -0500 Subject: [PATCH 022/329] Get tests to pass by creating a mock peer grading service --- .../peer_grading_service.py | 25 ++++++++++++++++++- lms/djangoapps/open_ended_grading/tests.py | 9 +++++-- lms/djangoapps/open_ended_grading/views.py | 8 +++++- lms/envs/common.py | 2 ++ lms/envs/test.py | 1 + 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 6b35f55f01..37611df4f9 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -15,6 +15,26 @@ from xmodule.course_module import CourseDescriptor log = logging.getLogger(__name__) +class MockPeerGradingService(object): + # TODO: make this return real results + def get_next_submission(self, problem_location, grader_id): + return {'success': true} + + def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key): + return {'success': true} + + def is_student_calibrated(self, problem_location, grader_id): + return {'success': true} + + def show_calibration_essay(self, problem_location, grader_id): + return {'success': true} + + def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): + return {'success': true} + + def get_problem_list(self, course_id, grader_id): + return {'success': true} + class PeerGradingService(GradingService): """ Interface with the grading controller for peer grading @@ -78,7 +98,10 @@ def peer_grading_service(): if _service is not None: return _service - _service = PeerGradingService(settings.PEER_GRADING_INTERFACE) + if settings.MOCK_PEER_GRADING: + _service = MockPeerGradingService() + else: + _service = PeerGradingService(settings.PEER_GRADING_INTERFACE) return _service diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 0636452779..30a58f6ee8 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -6,7 +6,7 @@ Replace this with more appropriate tests for your application. """ from django.test import TestCase -from instructor import staff_grading_service +from open_ended_grading import staff_grading_service from django.core.urlresolvers import reverse from django.contrib.auth.models import Group @@ -14,6 +14,11 @@ from courseware.access import _course_staff_group_name import courseware.tests.tests as ct from xmodule.modulestore.django import modulestore import xmodule.modulestore.django +from nose import SkipTest +from mock import patch, Mock +import json + +from override_settings import override_settings _mock_service = staff_grading_service.MockStaffGradingService() @@ -45,7 +50,7 @@ class TestStaffGradingService(ct.PageLoader): make_instructor(self.toy) - self.mock_service = staff_grading_service.grading_service() + self.mock_service = staff_grading_service.staff_grading_service() self.logout() diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 1026888987..887fe82aec 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -28,6 +28,7 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location from peer_grading_service import PeerGradingService +from peer_grading_service import MockPeerGradingService from grading_service import GradingServiceError import json import track.views @@ -38,7 +39,10 @@ from .staff_grading import StaffGrading log = logging.getLogger(__name__) template_imports = {'urllib': urllib} -peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) +if settings.MOCK_PEER_GRADING: + peer_gs = MockPeerGradingService() +else: + peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) @cache_control(no_cache=True, no_store=True, must_revalidate=True) def staff_grading(request, course_id): @@ -62,6 +66,7 @@ def staff_grading(request, course_id): 'staff_access': True, }) +@cache_control(no_cache=True, no_store=True, must_revalidate=True) def peer_grading(request, course_id): ''' Show a peer grading interface @@ -104,6 +109,7 @@ def peer_grading(request, course_id): 'staff_access': False, }) +@cache_control(no_cache=True, no_store=True, must_revalidate=True) def peer_grading_problem(request, course_id): ''' Show individual problem interface diff --git a/lms/envs/common.py b/lms/envs/common.py index 3b83b708aa..2354975cf0 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -333,6 +333,8 @@ STAFF_GRADING_INTERFACE = None # Used for testing, debugging MOCK_STAFF_GRADING = False +################################# Peer grading config ##################### +PEER_GRADING_INTERFACE = None ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' diff --git a/lms/envs/test.py b/lms/envs/test.py index c72c8b98bf..e9e4a43c6f 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -62,6 +62,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds # Don't rely on a real staff grading backend MOCK_STAFF_GRADING = True +MOCK_PEER_GRADING = True # TODO (cpennington): We need to figure out how envs/test.py can inject things # into common.py so that we don't have to repeat this sort of thing From b7473f8017abe48a36c55f565000c63c67ac038d Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 7 Jan 2013 15:40:01 -0500 Subject: [PATCH 023/329] Make mock service return useful data --- .../peer_grading_service.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 37611df4f9..4b4601fa91 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -16,24 +16,41 @@ from xmodule.course_module import CourseDescriptor log = logging.getLogger(__name__) class MockPeerGradingService(object): - # TODO: make this return real results def get_next_submission(self, problem_location, grader_id): - return {'success': true} + return json.dumps({'success': True, + 'submission_id':1, + 'submission_key': "", + 'student_response': 'fake student response', + 'prompt': 'fake submission prompt', + 'rubric': 'fake rubric', + 'max_score': 4}) def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key): - return {'success': true} + return json.dumps({'success': True}) def is_student_calibrated(self, problem_location, grader_id): - return {'success': true} + return json.dumps({'success': True, 'calibrated': True}) def show_calibration_essay(self, problem_location, grader_id): - return {'success': true} + return json.dumps({'success': True, + 'submission_id':1, + 'submission_key': '', + 'student_response': 'fake student response', + 'prompt': 'fake submission prompt', + 'rubric': 'fake rubric', + 'max_score': 4}) def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): - return {'success': true} + return {'success': True, 'actual_score': 2} def get_problem_list(self, course_id, grader_id): - return {'success': true} + return json.dumps({'success': True, + 'problem_list': [ + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', \ + 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}), + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', \ + 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}) + ]}) class PeerGradingService(GradingService): """ From bbc5cd50bc618371f481d946007c198ef6d2868c Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 7 Jan 2013 15:49:48 -0500 Subject: [PATCH 024/329] Add in default value for MOCK_PEER_GRADING --- lms/envs/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/envs/common.py b/lms/envs/common.py index 2354975cf0..88cf09502d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -335,6 +335,7 @@ MOCK_STAFF_GRADING = False ################################# Peer grading config ##################### PEER_GRADING_INTERFACE = None +MOCK_PEER_GRADING = False ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' From 02247172a2fb5fd83d0eb5f81e5f6faaab2c162d Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 7 Jan 2013 15:55:14 -0500 Subject: [PATCH 025/329] Fix some interstitial page CSS --- lms/static/sass/course/_staff_grading.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index b99056a7f5..816c0efd50 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -157,8 +157,13 @@ div.peer-grading{ } } - .interstitial-grading + .interstitial-page { + text-align: center; + input[type=button] + { + margin-top: 20px; + } } padding: 40px; } From 19401f29d917f261aa968e396e975e7f48fcdf87 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 8 Jan 2013 10:22:07 -0500 Subject: [PATCH 026/329] Minor javascript updates --- lms/static/coffee/src/peer_grading/peer_grading_problem.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0ffb5dc76d..85b5f064a4 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -325,6 +325,7 @@ class PeerGradingProblem @interstitial_page.show() render_error: (error_message) => @error_container.show() + @calibration_feedback_panel.hide() @error_container.html(error_message) @content_panel.hide() @@ -350,7 +351,7 @@ class PeerGradingProblem -mock_backend = true +mock_backend = false ajax_url = $('.peer-grading').data('ajax_url') backend = new PeerGradingProblemBackend(ajax_url, mock_backend) $(document).ready(() -> new PeerGradingProblem(backend)) From f0f25296b2cfe050e42161eb5caa530dedb61de3 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 8 Jan 2013 15:54:49 -0500 Subject: [PATCH 027/329] Update documentation --- .../peer_grading_service.py | 94 ++++++++++++++++--- .../staff_grading_service.py | 2 +- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 4b4601fa91..0b75997d91 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -144,7 +144,21 @@ def _check_post(request): def get_next_submission(request, course_id): """ - TODO: fill in this documentation + Makes a call to the grading controller for the next essay that should be graded + Returns a json dict with the following keys: + + 'success': bool + + 'submission_id': a unique identifier for the submission, to be passed back + with the grade. + + 'submission': the submission, rendered as read-only html for grading + + 'rubric': the rubric, also rendered as html. + + 'submission_key': a key associated with the submission for validation reasons + + 'error': if success is False, will have an error message with more info. """ _check_post(request) required = set(['location']) @@ -155,12 +169,10 @@ def get_next_submission(request, course_id): p = request.POST location = p['location'] - return HttpResponse(_get_next_submission(course_id, request.user.id, location), - mimetype="application/json") - -def _get_next_submission(course_id, grader_id, location): try: - return peer_grading_service().get_next_submission(location, grader_id) + response = peer_grading_service().get_next_submission(location, grader_id) + return HttpResponse(response, + mimetype="application/json") except GradingServiceError: log.exception("Error from grading service. server url: {0}" .format(staff_grading_service().url)) @@ -169,7 +181,18 @@ def _get_next_submission(course_id, grader_id, location): def save_grade(request, course_id): """ - TODO: fill in this documentation + Saves the grade of a given submission. + Input: + The request should have the following keys: + location - problem location + submission_id - id associated with this submission + submission_key - submission key given for validation purposes + score - the grade that was given to the submission + feedback - the feedback from the student + Returns + A json object with the following keys: + success: bool indicating whether the save was a success + 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']) @@ -197,7 +220,20 @@ def save_grade(request, course_id): def is_student_calibrated(request, course_id): """ - TODO: fill in this documentation + Calls the grading controller to see if the given student is calibrated + on the given problem + + Input: + In the request, we need the following arguments: + location - problem location + + Returns: + Json object with the following keys + success - bool indicating whether or not the call was successful + calibrated - true if the grader has fully calibrated and can now move on to grading + - false if the grader is still working on calibration problems + total_calibrated_on_so_far - the number of calibration essays for this problem + that this grader has graded """ _check_post(request) required = set(['location']) @@ -221,7 +257,26 @@ def is_student_calibrated(request, course_id): def show_calibration_essay(request, course_id): """ - TODO: fill in this documentation + Fetch the next calibration essay from the grading controller and return it + Inputs: + In the request + location - problem location + + Returns: + A json dict with the following keys + 'success': bool + + 'submission_id': a unique identifier for the submission, to be passed back + with the grade. + + 'submission': the submission, rendered as read-only html for grading + + 'rubric': the rubric, also rendered as html. + + 'submission_key': a key associated with the submission for validation reasons + + 'error': if success is False, will have an error message with more info. + """ _check_post(request) @@ -233,12 +288,9 @@ def show_calibration_essay(request, course_id): grader_id = request.user.id p = request.POST location = p['location'] - return HttpResponse(_next_calibration_essay(course_id, grader_id, location), - mimetype="application/json") - -def _next_calibration_essay(course_id, grader_id, location): try: - return peer_grading_service().show_calibration_essay(location, grader_id) + response = peer_grading_service().show_calibration_essay(location, grader_id) + return HttpResponse(response, mimetype="application/json") except GradingServiceError: log.exception("Error from grading service. server url: {0}" .format(staff_grading_service().url)) @@ -248,6 +300,20 @@ def _next_calibration_essay(course_id, grader_id, location): def save_calibration_essay(request, course_id): """ + Saves the grader's grade of a given calibration. + Input: + The request should have the following keys: + location - problem location + submission_id - id associated with this submission + submission_key - submission key given for validation purposes + score - the grade that was given to the submission + feedback - the feedback from the student + Returns + A json object with the following keys: + success: bool indicating whether the save was a success + error: if there was an error in the submission, this is the error message + actual_score: the score that the instructor gave to this calibration essay + """ _check_post(request) diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 5d56a90064..f2d4c5ee19 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -178,7 +178,7 @@ def _check_access(user, course_id): def get_next(request, course_id): """ Get the next thing to grade for course_id and with the location specified - in the . + in the request. Returns a json dict with the following keys: From 950d39838729ba77a94fda7b5c2c582a51901f95 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 9 Jan 2013 10:19:42 -0500 Subject: [PATCH 028/329] Updates to the CSS --- .../src/peer_grading/peer_grading_problem.coffee | 2 ++ lms/static/sass/course/_staff_grading.scss | 15 ++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) 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 85b5f064a4..119144d96a 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -129,6 +129,8 @@ class PeerGradingProblem @interstitial_page_button = $('.interstitial-page-button') Collapsible.setCollapsibles(@content_panel) + + # Set up the click event handlers @action_button.click -> document.location.reload(true) @calibration_feedback_button.click => @calibration_feedback_panel.hide() diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss index 816c0efd50..92fa760d4a 100644 --- a/lms/static/sass/course/_staff_grading.scss +++ b/lms/static/sass/course/_staff_grading.scss @@ -120,29 +120,22 @@ div.peer-grading{ .calibration-panel { float:left; - width:47%; + width:48%; } .grading-panel { float:right; - width: 47%; + width: 48%; } .current-state { - background: #0F6B8A; + background: #1D9DD9; h3, p { color: white; } } - &:after - { - content:"."; - display:block; - height:0; - visibility: hidden; - clear:both; - } + @include clearfix; } From c4d1b2e643aacad659076a6f6bfef8449efa72b2 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 9 Jan 2013 14:41:23 -0500 Subject: [PATCH 029/329] Use correct user id and make the reload button a back button --- .../open_ended_grading/peer_grading_service.py | 11 ++++++----- .../src/peer_grading/peer_grading_problem.coffee | 2 +- lms/templates/peer_grading/peer_grading_problem.html | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 0b75997d91..b8d8d2dbe6 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -12,6 +12,7 @@ from grading_service import GradingServiceError from courseware.access import has_access from util.json_request import expect_json from xmodule.course_module import CourseDescriptor +from student.models import unique_id_for_user log = logging.getLogger(__name__) @@ -165,7 +166,7 @@ def get_next_submission(request, course_id): success, message = _check_required(request, required) if not success: return _err_response(message) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] @@ -199,7 +200,7 @@ def save_grade(request, course_id): success, message = _check_required(request, required) if not success: return _err_response(message) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] submission_id = p['submission_id'] @@ -240,7 +241,7 @@ def is_student_calibrated(request, course_id): success, message = _check_required(request, required) if not success: return _err_response(message) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] @@ -285,7 +286,7 @@ def show_calibration_essay(request, course_id): if not success: return _err_response(message) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] try: @@ -321,7 +322,7 @@ def save_calibration_essay(request, course_id): success, message = _check_required(request, required) if not success: return _err_response(message) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] calibration_essay_id = p['submission_id'] 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 85b5f064a4..77cdd04b15 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -129,7 +129,7 @@ class PeerGradingProblem @interstitial_page_button = $('.interstitial-page-button') Collapsible.setCollapsibles(@content_panel) - @action_button.click -> document.location.reload(true) + @action_button.click -> history.back() @calibration_feedback_button.click => @calibration_feedback_panel.hide() @grading_wrapper.show() diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 9c11574f8d..d493e84ace 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -107,6 +107,6 @@
- + From 13c692c29b538e4084edaf68d69f8d6abbb77405 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 9 Jan 2013 14:59:59 -0500 Subject: [PATCH 030/329] Show back button when we see an error. --- lms/static/coffee/src/peer_grading/peer_grading_problem.coffee | 1 + 1 file changed, 1 insertion(+) 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 3a7b7c515a..639fcca947 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -330,6 +330,7 @@ class PeerGradingProblem @calibration_feedback_panel.hide() @error_container.html(error_message) @content_panel.hide() + @action_button.show() show_submit_button: () => @submit_button.show() From 19bc6574871c507484af69c6541e9530bf7fef73 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 12:44:54 -0500 Subject: [PATCH 031/329] Use correct version of the grader id. --- .../open_ended_grading/staff_grading_service.py | 9 +++++---- lms/djangoapps/open_ended_grading/views.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index f2d4c5ee19..521983eeee 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -16,6 +16,7 @@ from django.http import HttpResponse, Http404 from courseware.access import has_access from util.json_request import expect_json from xmodule.course_module import CourseDescriptor +from student.models import unique_id_for_user log = logging.getLogger(__name__) @@ -206,11 +207,11 @@ def get_next(request, course_id): if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST location = p['location'] - return HttpResponse(_get_next(course_id, request.user.id, location), + return HttpResponse(_get_next(course_id, grader_id, location), mimetype="application/json") @@ -238,7 +239,7 @@ def get_problem_list(request, course_id): """ _check_access(request.user, course_id) try: - response = staff_grading_service().get_problem_list(course_id, request.user.id) + response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user)) return HttpResponse(response, mimetype="application/json") except GradingServiceError: @@ -287,7 +288,7 @@ def save_grade(request, course_id): return _err_response('Missing required keys {0}'.format( ', '.join(missing))) - grader_id = request.user.id + grader_id = unique_id_for_user(request.user) p = request.POST diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 887fe82aec..e1aaf7011f 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -21,6 +21,7 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_R from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze from student.models import CourseEnrollment +from student.models import unique_id_for_user from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -78,7 +79,7 @@ def peer_grading(request, course_id): error_text = "" problem_list = [] try: - problem_list_text = peer_gs.get_problem_list(course_id, request.user.id) + problem_list_text = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user)) problem_list_json = json.loads(problem_list_text) success = problem_list_json['success'] if 'error' in problem_list_json: From 4909b84966242a41c4cabd75ecc1007f2f2b4ba8 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 14:51:32 -0500 Subject: [PATCH 032/329] Clean up code and make the arguments for get and post more logical. --- .../open_ended_grading/grading_service.py | 4 ++-- .../peer_grading_service.py | 24 ++++++++++++------- .../staff_grading_service.py | 6 ++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/grading_service.py b/lms/djangoapps/open_ended_grading/grading_service.py index 3c92c5bddd..96bd931448 100644 --- a/lms/djangoapps/open_ended_grading/grading_service.py +++ b/lms/djangoapps/open_ended_grading/grading_service.py @@ -44,7 +44,7 @@ class GradingService(object): return response.json - def post(self, url, allow_redirects, data): + def post(self, url, data, allow_redirects=False): """ Make a post request to the grading controller """ @@ -58,7 +58,7 @@ class GradingService(object): return r.text - def get(self, url, allow_redirects, params): + def get(self, url, params, allow_redirects=False): """ Make a get request to the grading controller """ diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index b8d8d2dbe6..14065d54e4 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -16,6 +16,10 @@ from student.models import unique_id_for_user log = logging.getLogger(__name__) +""" +This is a mock peer grading service that can be used for unit tests +without making actual service calls to the grading controller +""" class MockPeerGradingService(object): def get_next_submission(self, problem_location, grader_id): return json.dumps({'success': True, @@ -26,7 +30,8 @@ class MockPeerGradingService(object): 'rubric': 'fake rubric', 'max_score': 4}) - def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key): + def save_grade(self, location, grader_id, submission_id, + score, feedback, submission_key): return json.dumps({'success': True}) def is_student_calibrated(self, problem_location, grader_id): @@ -41,15 +46,16 @@ class MockPeerGradingService(object): 'rubric': 'fake rubric', 'max_score': 4}) - def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): + def save_calibration_essay(self, problem_location, grader_id, + calibration_essay_id, submission_key, score, feedback): return {'success': True, 'actual_score': 2} def get_problem_list(self, course_id, grader_id): return json.dumps({'success': True, 'problem_list': [ - json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', \ + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}), - json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', \ + json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}) ]}) @@ -78,15 +84,15 @@ class PeerGradingService(GradingService): 'feedback' : feedback, 'submission_key': submission_key, 'location': location} - return self.post(self.save_grade_url, False, data) + return self.post(self.save_grade_url, data) def is_student_calibrated(self, problem_location, grader_id): params = {'problem_id' : problem_location, 'student_id': grader_id} - return self.get(self.is_student_calibrated_url, False, params) + return self.get(self.is_student_calibrated_url, params) def show_calibration_essay(self, problem_location, grader_id): params = {'problem_id' : problem_location, 'student_id': grader_id} - return self.get(self.show_calibration_essay_url, False, params) + return self.get(self.show_calibration_essay_url, params) def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): data = {'location': problem_location, @@ -95,11 +101,11 @@ class PeerGradingService(GradingService): 'submission_key': submission_key, 'score': score, 'feedback': feedback} - return self.post(self.save_calibration_essay_url, False, data) + return self.post(self.save_calibration_essay_url, data) def get_problem_list(self, course_id, grader_id): params = {'course_id': course_id, 'student_id': grader_id} - response = self.get(self.get_problem_list_url, False, params) + response = self.get(self.get_problem_list_url, params) return response diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 521983eeee..5c6cec17eb 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -85,7 +85,7 @@ class StaffGradingService(GradingService): GradingServiceError: something went wrong with the connection. """ params = {'course_id': course_id,'grader_id': grader_id} - return self.get(self.get_problem_list_url, False, params) + return self.get(self.get_problem_list_url, params) def get_next(self, course_id, location, grader_id): @@ -107,7 +107,6 @@ class StaffGradingService(GradingService): GradingServiceError: something went wrong with the connection. """ return self.get(self.get_next_url, - allow_redirects=False, params={'location': location, 'grader_id': grader_id}) @@ -131,8 +130,7 @@ class StaffGradingService(GradingService): 'grader_id': grader_id, 'skipped': skipped} - return self.post(self.save_grade_url, data=data, - allow_redirects=False) + return self.post(self.save_grade_url, data=data) # don't initialize until staff_grading_service() is called--means that just # importing this file doesn't create objects that may not have the right config From 0bfb1feed4af59965a9187351ffaf3507f28bd41 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 15:06:16 -0500 Subject: [PATCH 033/329] Log better exceptions --- .../peer_grading_service.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 14065d54e4..5da243b0b5 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -181,8 +181,8 @@ def get_next_submission(request, course_id): return HttpResponse(response, mimetype="application/json") except GradingServiceError: - log.exception("Error from grading service. server url: {0}" - .format(staff_grading_service().url)) + log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}" + .format(staff_grading_service().url, location, grader_id)) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -218,8 +218,11 @@ def save_grade(request, course_id): score, feedback, submission_key) return HttpResponse(response, mimetype="application/json") except GradingServiceError: - log.exception("Error from grading service. server url: {0}" - .format(staff_grading_service().url)) + log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2}, + submission_key: {3}, score: {4}""" + .format(staff_grading_service().url, + location, submission_id, submission_key, score) + ) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -255,8 +258,8 @@ def is_student_calibrated(request, course_id): response = peer_grading_service().is_student_calibrated(location, grader_id) return HttpResponse(response, mimetype="application/json") except GradingServiceError: - log.exception("Error from grading service. server url: {0}" - .format(staff_grading_service().url)) + log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}" + .format(staff_grading_service().url, grader_id, location)) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -299,8 +302,8 @@ def show_calibration_essay(request, course_id): response = peer_grading_service().show_calibration_essay(location, grader_id) return HttpResponse(response, mimetype="application/json") except GradingServiceError: - log.exception("Error from grading service. server url: {0}" - .format(staff_grading_service().url)) + log.exception("Error from grading service. server url: {0}, location: {0}" + .format(staff_grading_service().url, location)) return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -340,5 +343,5 @@ def save_calibration_essay(request, course_id): response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, submission_key, score, feedback) return HttpResponse(response, mimetype="application/json") except GradingServiceError: - log.exception("Error saving calibration grade") + 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') From 084a3c33c8c821568a85c1b6b66db1d9147c26d3 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 15:20:46 -0500 Subject: [PATCH 034/329] Update comments and remove some unnecessary code --- lms/djangoapps/open_ended_grading/tests.py | 5 ++--- lms/djangoapps/open_ended_grading/views.py | 25 +--------------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 30a58f6ee8..0c4376a44b 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -1,8 +1,7 @@ """ -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". +Tests for open ended grading interfaces -Replace this with more appropriate tests for your application. +django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading """ from django.test import TestCase diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index e1aaf7011f..d3ce7e167c 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -1,39 +1,20 @@ # Grading Views -from collections import defaultdict -import csv import logging -import os import urllib from django.conf import settings -from django.contrib.auth.models import User, Group -from django.http import HttpResponse -from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response from django.core.urlresolvers import reverse -from courseware import grades -from courseware.access import has_access, get_access_group_name -from courseware.courses import get_course_with_access -from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA -from django_comment_client.utils import has_forum_access -from psychometrics import psychoanalyze -from student.models import CourseEnrollment from student.models import unique_id_for_user -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem -from xmodule.modulestore.search import path_to_location +from courseware.courses import get_course_with_access from peer_grading_service import PeerGradingService from peer_grading_service import MockPeerGradingService from grading_service import GradingServiceError import json -import track.views - from .staff_grading import StaffGrading @@ -52,14 +33,11 @@ def staff_grading(request, course_id): """ course = get_course_with_access(request.user, course_id, 'staff') - grading = StaffGrading(course) - ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) if not ajax_url.endswith('/'): ajax_url += '/' return render_to_response('instructor/staff_grading.html', { - 'view_html': '', 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, @@ -99,7 +77,6 @@ def peer_grading(request, course_id): ajax_url += '/' return render_to_response('peer_grading/peer_grading.html', { - 'view_html': '', 'course': course, 'course_id': course_id, 'ajax_url': ajax_url, From ef6d77b116c28c0d050d9309d9234d2643d2145d Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 15:33:17 -0500 Subject: [PATCH 035/329] Clean up names and refactor out some common logic --- lms/djangoapps/open_ended_grading/views.py | 37 +++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index d3ce7e167c..858c9a4fd5 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -26,6 +26,18 @@ if settings.MOCK_PEER_GRADING: else: peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE) +""" +Reverses the URL from the name and the course id, and then adds a trailing slash if +it does not exist yet + +""" +def _reverse_with_slash(url_name, course_id): + ajax_url = reverse(url_name, kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + return ajax_url + + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def staff_grading(request, course_id): """ @@ -33,9 +45,7 @@ def staff_grading(request, course_id): """ course = get_course_with_access(request.user, course_id, 'staff') - ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) - if not ajax_url.endswith('/'): - ajax_url += '/' + ajax_url = _reverse_with_slash('staff_grading', course_id) return render_to_response('instructor/staff_grading.html', { 'course': course, @@ -57,24 +67,23 @@ def peer_grading(request, course_id): error_text = "" problem_list = [] try: - problem_list_text = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user)) - problem_list_json = json.loads(problem_list_text) - success = problem_list_json['success'] - if 'error' in problem_list_json: - error_text = problem_list_json['error'] + problem_list_json = peer_gs.get_problem_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_json['problem_list'] + problem_list = problem_list_dict['problem_list'] 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('peer_grading', kwargs={'course_id': course_id}) - if not ajax_url.endswith('/'): - ajax_url += '/' + ajax_url = _reverse_with_slash('peer_grading', course_id) return render_to_response('peer_grading/peer_grading.html', { 'course': course, @@ -95,9 +104,7 @@ def peer_grading_problem(request, course_id): course = get_course_with_access(request.user, course_id, 'load') problem_location = request.GET.get("location") - ajax_url = reverse('peer_grading', kwargs={'course_id': course_id}) - if not ajax_url.endswith('/'): - ajax_url += '/' + ajax_url = _reverse_with_slash('peer_grading', course_id) return render_to_response('peer_grading/peer_grading_problem.html', { 'view_html': '', From 8f21d7a738a415d58acc8fb7d0712caad7e7ee43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 10 Jan 2013 14:48:27 -0500 Subject: [PATCH 036/329] Add property to course module to check if a course is new The property can be set in the policy metadata. If it is not specified then it is set to true if the course has not started yet. Also adds a property to check how many days are left until the course starts. --- common/djangoapps/student/views.py | 2 +- common/lib/xmodule/xmodule/course_module.py | 41 +++++++-- .../xmodule/tests/test_course_module.py | 90 +++++++++++++++++++ lms/djangoapps/courseware/courses.py | 30 ------- lms/djangoapps/courseware/views.py | 4 +- lms/templates/course.html | 2 +- 6 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 common/lib/xmodule/xmodule/tests/test_course_module.py diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 06c59d7937..39805fd85f 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -78,7 +78,7 @@ def index(request, extra_context={}, user=None): courses = get_courses(None, domain=domain) # Sort courses by how far are they from they start day - key = lambda course: course.metadata['days_to_start'] + key = lambda course: course.days_until_start courses = sorted(courses, key=key, reverse=True) # Get the 3 most recent news diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5253d2976f..163e40b343 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,9 +1,9 @@ -from fs.errors import ResourceNotFoundError import logging from lxml import etree -from path import path # NOTE (THK): Only used for detecting presence of syllabus +from path import path # NOTE (THK): Only used for detecting presence of syllabus import requests import time +from datetime import datetime from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy @@ -13,6 +13,7 @@ from xmodule.timeparse import parse_time, stringify_time log = logging.getLogger(__name__) + class CourseDescriptor(SequenceDescriptor): module_class = SequenceModule @@ -165,6 +166,38 @@ class CourseDescriptor(SequenceDescriptor): def show_calculator(self): return self.metadata.get("show_calculator", None) == "Yes" + @property + def is_new(self): + # The course is "new" if either if the metadata flag is_new is + # true or if the course has not started yet + flag = self.metadata.get('is_new', None) + if flag is None: + return self.days_until_start > 1 + elif isinstance(flag, basestring): + return flag.lower() in ['true', 'yes', 'y'] + else: + return bool(flag) + + @property + def days_until_start(self): + def convert_to_datetime(timestamp): + return datetime.fromtimestamp(time.mktime(timestamp)) + + start_date = convert_to_datetime(self.start) + + # Try to use course advertised date if we can parse it + advertised_start = self.metadata.get('advertised_start', None) + if advertised_start: + try: + start_date = datetime.strptime(advertised_start, + "%Y-%m-%dT%H:%M") + except ValueError: + pass # Invalid date, keep using 'start'' + + now = convert_to_datetime(time.gmtime()) + days_until_start = (start_date - now).days + return days_until_start + @lazyproperty def grading_context(self): """ @@ -244,7 +277,6 @@ class CourseDescriptor(SequenceDescriptor): raise ValueError("{0} is not a course location".format(loc)) return "/".join([loc.org, loc.course, loc.name]) - @property def id(self): """Return the course_id for this course""" @@ -258,7 +290,7 @@ class CourseDescriptor(SequenceDescriptor): # form text... if parsed_advertised_start is None and \ ('advertised_start' in self.metadata): - return self.metadata['advertised_start'] + return self.metadata['advertised_start'] displayed_start = parsed_advertised_start or self.start @@ -341,4 +373,3 @@ class CourseDescriptor(SequenceDescriptor): @property def org(self): return self.location.org - diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py new file mode 100644 index 0000000000..63eaec1f61 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -0,0 +1,90 @@ +import unittest +from time import strptime, gmtime +from fs.memoryfs import MemoryFS + +from mock import Mock, patch + +from xmodule.modulestore.xml import ImportSystem, XMLModuleStore + + +ORG = 'test_org' +COURSE = 'test_course' + +NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00') + + +class DummySystem(ImportSystem): + @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS()) + def __init__(self, load_error_modules): + + xmlstore = XMLModuleStore("data_dir", course_dirs=[], + load_error_modules=load_error_modules) + course_id = "/".join([ORG, COURSE, 'test_run']) + course_dir = "test_dir" + policy = {} + error_tracker = Mock() + parent_tracker = Mock() + + super(DummySystem, self).__init__( + xmlstore, + course_id, + course_dir, + policy, + error_tracker, + parent_tracker, + load_error_modules=load_error_modules, + ) + + +class IsNewCourseTestCase(unittest.TestCase): + """Make sure the property is_new works on courses""" + @staticmethod + def get_dummy_course(start, is_new=None, load_error_modules=True): + """Get a dummy course""" + + system = DummySystem(load_error_modules) + is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower() + + start_xml = ''' + + + Two houses, ... + + + '''.format(org=ORG, course=COURSE, start=start, is_new=is_new) + + return system.process_xml(start_xml) + + @patch('xmodule.course_module.time.gmtime') + def test_non_started_yet(self, gmtime_mock): + descriptor = self.get_dummy_course(start='2013-01-05T12:00') + gmtime_mock.return_value = NOW + assert(descriptor.is_new == True) + assert(descriptor.days_until_start == 4) + + @patch('xmodule.course_module.time.gmtime') + def test_already_started(self, gmtime_mock): + gmtime_mock.return_value = NOW + + descriptor = self.get_dummy_course(start='2012-12-02T12:00') + assert(descriptor.is_new == False) + assert(descriptor.days_until_start < 0) + + @patch('xmodule.course_module.time.gmtime') + def test_is_new_set(self, gmtime_mock): + gmtime_mock.return_value = NOW + + descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) + assert(descriptor.is_new == True) + assert(descriptor.days_until_start < 0) + + descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False) + assert(descriptor.is_new == False) + assert(descriptor.days_until_start > 0) + + descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True) + assert(descriptor.is_new == True) + assert(descriptor.days_until_start > 0) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index dc530bdebc..7c0d30ebd8 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -233,35 +233,5 @@ def get_courses(user, domain=None): courses = branding.get_visible_courses(domain) courses = [c for c in courses if has_access(user, c, 'see_exists')] - # Add metadata about the start day and if the course is new - for course in courses: - days_to_start = _get_course_days_to_start(course) - - metadata = course.metadata - metadata['days_to_start'] = days_to_start - metadata['is_new'] = course.metadata.get('is_new', days_to_start > 1) - courses = sorted(courses, key=lambda course:course.number) return courses - - -def _get_course_days_to_start(course): - from datetime import datetime as dt - from time import mktime, gmtime - - convert_to_datetime = lambda ts: dt.fromtimestamp(mktime(ts)) - - start_date = convert_to_datetime(course.start) - - # If the course has a valid advertised date, use that instead - advertised_start = course.metadata.get('advertised_start', None) - if advertised_start: - try: - start_date = dt.strptime(advertised_start, "%Y-%m-%dT%H:%M") - except ValueError: - pass # Invalid date, keep using course.start - - now = convert_to_datetime(gmtime()) - days_to_start = (start_date - now).days - - return days_to_start diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index f6e87dfe9f..9e52e2b281 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -70,7 +70,7 @@ def courses(request): courses = get_courses(request.user, domain=request.META.get('HTTP_HOST')) # Sort courses by how far are they from they start day - key = lambda course: course.metadata['days_to_start'] + key = lambda course: course.days_until_start courses = sorted(courses, key=key, reverse=True) return render_to_response("courseware/courses.html", {'courses': courses}) @@ -440,7 +440,7 @@ def university_profile(request, org_id): domain=request.META.get('HTTP_HOST'))[org_id] # Sort courses by how far are they from they start day - key = lambda course: course.metadata['days_to_start'] + key = lambda course: course.days_until_start courses = sorted(courses, key=key, reverse=True) context = dict(courses=courses, org_id=org_id) diff --git a/lms/templates/course.html b/lms/templates/course.html index a3217d2da5..a2eff572e1 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -5,7 +5,7 @@ %> <%page args="course" />
- %if course.metadata.get('is_new'): + %if course.is_new: New %endif From 81bb2dc979f22cb157b2d437869370f4ea3cbae7 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 10 Jan 2013 16:45:19 -0500 Subject: [PATCH 037/329] Better and clearer comments along with some fixes for code review issues --- .../peer_grading_service.py | 8 ++++ .../src/peer_grading/peer_grading.coffee | 9 ++-- .../peer_grading/peer_grading_problem.coffee | 44 ++++++++++++++++--- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 5da243b0b5..859499ff7e 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -1,3 +1,11 @@ +""" +This module provides an interface on the grading-service backend +for peer grading + +Use peer_grading_service() to get the version specified +in settings.PEER_GRADING_INTERFACE + +""" import json import logging import requests diff --git a/lms/static/coffee/src/peer_grading/peer_grading.coffee b/lms/static/coffee/src/peer_grading/peer_grading.coffee index c20944252c..0736057df8 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading.coffee @@ -1,10 +1,13 @@ +# 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 PeerGrading - constructor: (backend) -> + constructor: () -> @error_container = $('.error-container') @error_container.toggle(not @error_container.is(':empty')) @message_container = $('.message-container') @message_container.toggle(not @message_container.is(':empty')) -mock_backend = false -$(document).ready(() -> new PeerGrading(mock_backend)) +$(document).ready(() -> new PeerGrading()) 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 77cdd04b15..e815a05d64 100644 --- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee +++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee @@ -1,3 +1,20 @@ +################################## +# +# This is the JS that renders the peer grading problem page. +# Fetches the correct problem and/or calibration essay +# and sends back the grades +# +# Should not be run when we don't have a location to send back +# to the server +# +# PeerGradingProblemBackend - +# makes all the ajax requests and provides a mock interface +# for testing purposes +# +# PeerGradingProblem - +# handles the rendering and user interactions with the interface +# +################################## class PeerGradingProblemBackend constructor: (ajax_url, mock_backend) -> @mock_backend = mock_backend @@ -8,7 +25,7 @@ class PeerGradingProblemBackend if @mock_backend callback(@mock(cmd, data)) else - # TODO: replace with postWithPrefix when that's loaded + # 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"}) @@ -90,13 +107,13 @@ class PeerGradingProblem @prompt_wrapper = $('.prompt-wrapper') @backend = backend - # ugly hack to prevent this code from trying to run on the - # general peer grading page - if( @prompt_wrapper.length == 0) - return # get the location of the problem @location = $('.peer-grading').data('location') + # prevent this code from trying to run + # when we don't have a location + if(!@location) + return # get the other elements we want to fill in @submission_container = $('.submission-container') @@ -180,6 +197,8 @@ class PeerGradingProblem # Callbacks for various events # ########## + + # called after we perform an is_student_calibrated check calibration_check_callback: (response) => if response.success # if we haven't been calibrating before @@ -199,12 +218,17 @@ class PeerGradingProblem else @render_error("Error contacting the grading service") + + # called after we submit a calibration score calibration_callback: (response) => if response.success @render_calibration_feedback(response) else if response.error @render_error(response.error) + else + @render_error("Error saving calibration score") + # called after we submit a submission score submission_callback: (response) => if response.success @is_calibrated_check() @@ -216,6 +240,7 @@ class PeerGradingProblem else @render_error("Error occurred while submitting grade") + # called after a grade is selected on the interface graded_callback: (event) => @grading_message.hide() @score = event.target.value @@ -240,6 +265,8 @@ class PeerGradingProblem @grading_panel.removeClass('current-state') # Display the right text + # both versions of the text are written into the template itself + # we only need to show/hide the correct ones at the correct time @calibration_panel.find('.calibration-text').show() @grading_panel.find('.calibration-text').show() @calibration_panel.find('.grading-text').hide() @@ -265,6 +292,8 @@ class PeerGradingProblem @grading_panel.addClass('current-state') # Display the correct text + # both versions of the text are written into the template itself + # we only need to show/hide the correct ones at the correct time @calibration_panel.find('.calibration-text').hide() @grading_panel.find('.calibration-text').hide() @calibration_panel.find('.grading-text').show() @@ -285,6 +314,7 @@ class PeerGradingProblem new_text += "

#{paragraph}

" return new_text + # render common information between calibration and grading render_submission_data: (response) => @content_panel.show() @@ -302,7 +332,6 @@ class PeerGradingProblem render_calibration_feedback: (response) => # display correct grade - #@grading_wrapper.hide() @calibration_feedback_panel.slideDown() calibration_wrapper = $('.calibration-feedback-wrapper') calibration_wrapper.html("

The score you gave was: #{@score}. The actual score is: #{response.actual_score}

") @@ -314,7 +343,7 @@ class PeerGradingProblem if score == actual_score calibration_wrapper.append("

Congratulations! Your score matches the actual score!

") else - calibration_wrapper.append("

Please try to understand the grading critera better so that you will be more accurate next time.

") + calibration_wrapper.append("

Please try to understand the grading critera better to be more accurate next time.

") # disable score selection and submission from the grading interface $("input[name='score-selection']").attr('disabled', true) @@ -323,6 +352,7 @@ class PeerGradingProblem render_interstitial_page: () => @content_panel.hide() @interstitial_page.show() + render_error: (error_message) => @error_container.show() @calibration_feedback_panel.hide() From 60f9143fa5c577a2136d7e027a252be9898dac39 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 12 Dec 2012 14:04:04 -0500 Subject: [PATCH 038/329] Minor change --- common/lib/xmodule/xmodule/capa_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d65fa1f40a..08f503f127 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -445,6 +445,7 @@ class CapaModule(XModule): return False + def update_score(self, get): """ Delivers grading response (e.g. from asynchronous code checking) to From 5858ec5e0d2a6d7e48ce0f2de6f87461c1c0e892 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 10:18:21 -0500 Subject: [PATCH 039/329] Begin making integrated module --- .../xmodule/combined_open_ended_module.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 common/lib/xmodule/xmodule/combined_open_ended_module.py diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py new file mode 100644 index 0000000000..044026311f --- /dev/null +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -0,0 +1,90 @@ +import copy +from fs.errors import ResourceNotFoundError +import itertools +import json +import logging +from lxml import etree +from lxml.html import rewrite_links +from path import path +import os +import sys + +from pkg_resources import resource_string + +from .capa_module import only_one, ComplexEncoder +from .editing_module import EditingDescriptor +from .html_checker import check_html +from progress import Progress +from .stringify import stringify_children +from .x_module import XModule +from .xml_module import XmlDescriptor +from xmodule.modulestore import Location +import self_assessment_module + +log = logging.getLogger("mitx.courseware") + +# Set the default number of max attempts. Should be 1 for production +# Set higher for debugging/testing +# attempts specified in xml definition overrides this. +MAX_ATTEMPTS = 1 + +# Set maximum available number of points. +# Overriden by max_score specified in xml. +MAX_SCORE = 1 + +class CombinedOpenEndedModule(XModule): + pass + +class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): + """ + Module for adding self assessment questions to courses + """ + mako_template = "widgets/html-edit.html" + module_class = CombinedOpenEndedModule + filename_extension = "xml" + + stores_state = True + has_score = True + template_dir_name = "combinedopenended" + + js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} + js_module_name = "HTMLEditingDescriptor" + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Pull out the rubric, prompt, and submitmessage into a dictionary. + + Returns: + { + 'rubric': 'some-html', + 'prompt': 'some-html', + 'submitmessage': 'some-html' + 'hintprompt': 'some-html' + } + """ + expected_children = ['task'] + for child in expected_children: + if len(xml_object.xpath(child)) == 0 : + raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) + + def parse(k): + """Assumes that xml_object has child k""" + return stringify_children(xml_object.xpath(k)[0]) + + return {'task': parse('task')} + + + def definition_to_xml(self, resource_fs): + '''Return an xml element representing this definition.''' + elt = etree.Element('selfassessment') + + def add_child(k): + child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + child_node = etree.fromstring(child_str) + elt.append(child_node) + + for child in ['task']: + add_child(child) + + return elt \ No newline at end of file From e0d12bcbfe426bc34ad4992d82ab0b1939dce2cd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 10:21:04 -0500 Subject: [PATCH 040/329] Add in some descriptions --- .../xmodule/combined_open_ended_module.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 044026311f..c85552d442 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -33,7 +33,62 @@ MAX_ATTEMPTS = 1 MAX_SCORE = 1 class CombinedOpenEndedModule(XModule): - pass + STATE_VERSION = 1 + + # states + INITIAL = 'initial' + ASSESSING = 'assessing' + DONE = 'done' + TASK_TYPES=["self", "ml", "instructor", "peer"] + + js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} + js_module_name = "SelfAssessment" + + 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) + + """ + Definition file should have multiple task blocks: + + Sample file: + + + + + + + + """ + + # Load instance state + if instance_state is not None: + instance_state = json.loads(instance_state) + else: + instance_state = {} + + instance_state = self.convert_state_to_current_format(instance_state) + + # History is a list of tuples of (answer, score, hint), where hint may be + # None for any element, and score and hint can be None for the last (current) + # element. + # Scores are on scale from 0 to max_score + self.history = instance_state.get('history', []) + + self.state = instance_state.get('state', 'initial') + + self.attempts = instance_state.get('attempts', 0) + self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) + + # Used for progress / grading. Currently get credit just for + # completion (doesn't matter if you self-assessed correct/incorrect). + self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) + + self.rubric = definition['rubric'] + self.prompt = definition['prompt'] + self.submit_message = definition['submitmessage'] + self.hint_prompt = definition['hintprompt'] class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ From 0be51cfa2d8e70a011e852c5a5a85b018be0cc95 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 10:52:18 -0500 Subject: [PATCH 041/329] Xmodule combined now working --- common/lib/xmodule/setup.py | 1 + .../xmodule/combined_open_ended_module.py | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index d3889bc388..34e4e658c9 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -19,6 +19,7 @@ setup( "abtest = xmodule.abtest_module:ABTestDescriptor", "book = xmodule.backcompat_module:TranslateCustomTagDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor", + "combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor" "course = xmodule.course_module:CourseDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index c85552d442..cbbfe156c3 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -42,7 +42,7 @@ class CombinedOpenEndedModule(XModule): TASK_TYPES=["self", "ml", "instructor", "peer"] js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} - js_module_name = "SelfAssessment" + js_module_name = "CombinedOpenEnded" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -54,7 +54,7 @@ class CombinedOpenEndedModule(XModule): Sample file: - + @@ -68,15 +68,14 @@ class CombinedOpenEndedModule(XModule): else: instance_state = {} - instance_state = self.convert_state_to_current_format(instance_state) - # History is a list of tuples of (answer, score, hint), where hint may be # None for any element, and score and hint can be None for the last (current) # element. # Scores are on scale from 0 to max_score - self.history = instance_state.get('history', []) + self.current_task = instance_state.get('current_task', 0) self.state = instance_state.get('state', 'initial') + self.problems = instance_state.get('problems', []) self.attempts = instance_state.get('attempts', 0) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) @@ -85,10 +84,16 @@ class CombinedOpenEndedModule(XModule): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) - self.rubric = definition['rubric'] - self.prompt = definition['prompt'] - self.submit_message = definition['submitmessage'] - self.hint_prompt = definition['hintprompt'] + self.tasks=definition['tasks'] + log.debug(self.tasks) + + def setup_next_task(self): + pass + + def get_html(self): + html = "" + + return rewrite_links(html, self.rewrite_content_links) class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ @@ -125,9 +130,9 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): def parse(k): """Assumes that xml_object has child k""" - return stringify_children(xml_object.xpath(k)[0]) + return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] - return {'task': parse('task')} + return {'tasks': parse('task')} def definition_to_xml(self, resource_fs): From 867deae6e36f6bff9a4229a4d549777b52cddccf Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 11:20:45 -0500 Subject: [PATCH 042/329] Bugfixes --- .../xmodule/combined_open_ended_module.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index cbbfe156c3..c5714ecc84 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -38,6 +38,7 @@ class CombinedOpenEndedModule(XModule): # states INITIAL = 'initial' ASSESSING = 'assessing' + INTERMEDIATE_DONE='intermediate_done' DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] @@ -72,7 +73,8 @@ class CombinedOpenEndedModule(XModule): # None for any element, and score and hint can be None for the last (current) # element. # Scores are on scale from 0 to max_score - self.current_task = instance_state.get('current_task', 0) + self.current_task_number = instance_state.get('current_task_number', 0) + self.tasks = instance_state.get('tasks', []) self.state = instance_state.get('state', 'initial') self.problems = instance_state.get('problems', []) @@ -84,16 +86,31 @@ class CombinedOpenEndedModule(XModule): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) - self.tasks=definition['tasks'] - log.debug(self.tasks) + self.task_xml=definition['task_xml'] + self.setup_next_task() + + def get_tag_name(self, xml): + tag=etree.fromstring(xml).tag + return tag def setup_next_task(self): - pass + if self.state in [self.ASSESSING, self.DONE]: + self.current_task=self.tasks[len(self.tasks)-1] + return True + + self.current_task_xml=self.task_xml[self.current_task_number] + current_task_type=self.get_tag_name(self.current_task_xml) + if current_task_type=="selfassessment": + self.current_task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(self.current_task_xml,self.system) + self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + return True def get_html(self): - html = "" + return self.current_task.get_html() - return rewrite_links(html, self.rewrite_content_links) + def handle_ajax(self, dispatch, get): + return self.current_task.handle_ajax(dispatch,get) class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ @@ -132,7 +149,7 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] - return {'tasks': parse('task')} + return {'task_xml': parse('task')} def definition_to_xml(self, resource_fs): From 4047106e48afb8a49664e7e85d3e8c26f34b7ac7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 11:38:55 -0500 Subject: [PATCH 043/329] Some progress with rendering --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index c5714ecc84..b8ad5fdaaa 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -102,7 +102,7 @@ class CombinedOpenEndedModule(XModule): current_task_type=self.get_tag_name(self.current_task_xml) if current_task_type=="selfassessment": self.current_task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(self.current_task_xml,self.system) + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) return True From e975174d662bc18e86528a66afa9ad7fba1e7289 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 11:49:33 -0500 Subject: [PATCH 044/329] Convert self-assessment away from xmodule --- common/lib/xmodule/setup.py | 1 - .../xmodule/combined_open_ended_module.py | 3 ++- .../xmodule/xmodule/self_assessment_module.py | 18 +++++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 34e4e658c9..c20b50ec66 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -29,7 +29,6 @@ setup( "problem = xmodule.capa_module:CapaDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor", - "selfassessment = xmodule.self_assessment_module:SelfAssessmentDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index b8ad5fdaaa..b1585f9c10 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -107,7 +107,8 @@ class CombinedOpenEndedModule(XModule): return True def get_html(self): - return self.current_task.get_html() + html = self.current_task.get_html(self.system) + return rewrite_links(html, self.rewrite_content_links) def handle_ajax(self, dispatch, get): return self.current_task.handle_ajax(dispatch,get) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index eb8a275d35..eef81182ea 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -38,7 +38,7 @@ MAX_ATTEMPTS = 1 # Overriden by max_score specified in xml. MAX_SCORE = 1 -class SelfAssessmentModule(XModule): +class SelfAssessmentModule(): """ States: @@ -66,9 +66,6 @@ class SelfAssessmentModule(XModule): 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) - """ Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, and two optional attributes: @@ -116,11 +113,11 @@ class SelfAssessmentModule(XModule): self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) - self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) + self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). - self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) + self._max_score = int(instance_state.get('max_score', MAX_SCORE)) self.rubric = definition['rubric'] self.prompt = definition['prompt'] @@ -224,7 +221,7 @@ class SelfAssessmentModule(XModule): """Can the module be reset?""" return self.state == self.DONE and self.attempts < self.max_attempts - def get_html(self): + def get_html(self, system): #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() @@ -235,17 +232,16 @@ class SelfAssessmentModule(XModule): context = { 'prompt': self.prompt, 'previous_answer': previous_answer, - 'ajax_url': self.system.ajax_url, + 'ajax_url': system.ajax_url, 'initial_rubric': self.get_rubric_html(), 'initial_hint': self.get_hint_html(), 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), } - html = self.system.render_template('self_assessment_prompt.html', context) - # cdodge: perform link substitutions for any references to course static content (e.g. images) - return rewrite_links(html, self.rewrite_content_links) + html = system.render_template('self_assessment_prompt.html', context) + return html def max_score(self): """ From cd764d3d7c6c2bbf209254427469ecfe9bc3632c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 11:55:12 -0500 Subject: [PATCH 045/329] Modify self-assessment to not be an xmodule --- .../xmodule/combined_open_ended_module.py | 4 +-- .../xmodule/xmodule/self_assessment_module.py | 27 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index b1585f9c10..6e3fdc1ddd 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -43,7 +43,7 @@ class CombinedOpenEndedModule(XModule): TASK_TYPES=["self", "ml", "instructor", "peer"] js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} - js_module_name = "CombinedOpenEnded" + js_module_name = "SelfAssessment" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -111,7 +111,7 @@ class CombinedOpenEndedModule(XModule): return rewrite_links(html, self.rewrite_content_links) def handle_ajax(self, dispatch, get): - return self.current_task.handle_ajax(dispatch,get) + return self.current_task.handle_ajax(dispatch,get, self.system) class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index eef81182ea..150ad571b7 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -233,8 +233,8 @@ class SelfAssessmentModule(): 'prompt': self.prompt, 'previous_answer': previous_answer, 'ajax_url': system.ajax_url, - 'initial_rubric': self.get_rubric_html(), - 'initial_hint': self.get_hint_html(), + 'initial_rubric': self.get_rubric_html(system), + 'initial_hint': self.get_hint_html(system), 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), @@ -270,7 +270,7 @@ class SelfAssessmentModule(): return None - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, get, system): """ This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST. @@ -292,7 +292,7 @@ class SelfAssessmentModule(): return 'Error' before = self.get_progress() - d = handlers[dispatch](get) + d = handlers[dispatch](get, system) after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -309,7 +309,7 @@ class SelfAssessmentModule(): return {'success': False, 'error': 'The problem state got out-of-sync'} - def get_rubric_html(self): + def get_rubric_html(self,system): """ Return the appropriate version of the rubric, based on the state. """ @@ -328,9 +328,9 @@ class SelfAssessmentModule(): else: raise ValueError("Illegal state '%r'" % self.state) - return self.system.render_template('self_assessment_rubric.html', context) + return system.render_template('self_assessment_rubric.html', context) - def get_hint_html(self): + def get_hint_html(self,system): """ Return the appropriate version of the hint view, based on state. """ @@ -354,7 +354,7 @@ class SelfAssessmentModule(): else: raise ValueError("Illegal state '%r'" % self.state) - return self.system.render_template('self_assessment_hint.html', context) + return system.render_template('self_assessment_hint.html', context) def get_message_html(self): """ @@ -366,7 +366,7 @@ class SelfAssessmentModule(): return """
{0}
""".format(self.submit_message) - def save_answer(self, get): + def save_answer(self, get, system): """ After the answer is submitted, show the rubric. @@ -397,10 +397,10 @@ class SelfAssessmentModule(): return { 'success': True, - 'rubric_html': self.get_rubric_html() + 'rubric_html': self.get_rubric_html(system) } - def save_assessment(self, get): + def save_assessment(self, get, system): """ Save the assessment. If the student said they're right, don't ask for a hint, and go straight to the done state. Otherwise, do ask for a hint. @@ -433,13 +433,13 @@ class SelfAssessmentModule(): d['allow_reset'] = self._allow_reset() else: self.change_state(self.REQUEST_HINT) - d['hint_html'] = self.get_hint_html() + d['hint_html'] = self.get_hint_html(system) d['state'] = self.state return d - def save_hint(self, get): + def save_hint(self, get, system): ''' Save the hint. Returns a dict { 'success': bool, @@ -465,7 +465,6 @@ class SelfAssessmentModule(): 'history': self.history, } } - self.system.track_function('save_hint', event_info) return {'success': True, 'message_html': self.get_message_html(), From 517c79e00a1110df073d4dbb60c5167d3f7b9edc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 12:25:10 -0500 Subject: [PATCH 046/329] Fixing state tracking --- .../xmodule/combined_open_ended_module.py | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 6e3fdc1ddd..67ba54fcc5 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -74,10 +74,9 @@ class CombinedOpenEndedModule(XModule): # element. # Scores are on scale from 0 to max_score self.current_task_number = instance_state.get('current_task_number', 0) - self.tasks = instance_state.get('tasks', []) + self.task_states= instance_state.get('task_states', []) self.state = instance_state.get('state', 'initial') - self.problems = instance_state.get('problems', []) self.attempts = instance_state.get('attempts', 0) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) @@ -94,24 +93,51 @@ class CombinedOpenEndedModule(XModule): return tag def setup_next_task(self): + current_task_state=None if self.state in [self.ASSESSING, self.DONE]: - self.current_task=self.tasks[len(self.tasks)-1] - return True + current_task_state=self.task_states[len(self.task_states)-1] + + log.debug(self.task_states) self.current_task_xml=self.task_xml[self.current_task_number] current_task_type=self.get_tag_name(self.current_task_xml) if current_task_type=="selfassessment": self.current_task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) - self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + if current_task_state is None: + self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING + else: + self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + return True def get_html(self): html = self.current_task.get_html(self.system) - return rewrite_links(html, self.rewrite_content_links) + return_html = rewrite_links(html, self.rewrite_content_links) + self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + return return_html def handle_ajax(self, dispatch, get): - return self.current_task.handle_ajax(dispatch,get, self.system) + return_html = self.current_task.handle_ajax(dispatch,get, self.system) + self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + return return_html + + def get_instance_state(self): + """ + Get the current score and state + """ + + state = { + 'version': self.STATE_VERSION, + 'current_task_number': self.current_task_number, + 'state': self.state, + 'task_states': self.task_states, + 'attempts': self.attempts, + } + + return json.dumps(state) class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ From b009d15dcc7e4f816348fde52fbba2238fbf534e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 28 Dec 2012 12:28:31 -0500 Subject: [PATCH 047/329] Self assessment wrapped into another module now works --- common/lib/xmodule/xmodule/self_assessment_module.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 150ad571b7..0c37384c16 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -457,21 +457,12 @@ class SelfAssessmentModule(): self.record_latest_hint(get['hint']) self.change_state(self.DONE) - # To the tracking logs! - event_info = { - 'selfassessment_id': self.location.url(), - 'state': { - 'version': self.STATE_VERSION, - 'history': self.history, - } - } - return {'success': True, 'message_html': self.get_message_html(), 'allow_reset': self._allow_reset()} - def reset(self, get): + def reset(self, get, system): """ If resetting is allowed, reset the state. From 2bbbe12ae8d58f6c3a476b4af5deefc44fca2a5d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 2 Jan 2013 13:13:05 -0500 Subject: [PATCH 048/329] Refactor instance state saving --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 7 +++++-- common/lib/xmodule/xmodule/open_ended_module.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 common/lib/xmodule/xmodule/open_ended_module.py diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 67ba54fcc5..aa759ea7dc 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -116,12 +116,15 @@ class CombinedOpenEndedModule(XModule): def get_html(self): html = self.current_task.get_html(self.system) return_html = rewrite_links(html, self.rewrite_content_links) - self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + self.update_task_states() return return_html + def update_task_states(self): + self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + def handle_ajax(self, dispatch, get): return_html = self.current_task.handle_ajax(dispatch,get, self.system) - self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + self.update_task_states() return return_html def get_instance_state(self): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -0,0 +1 @@ + From ae0623ab9ba08c7a28f51056fbe14fd917522291 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 2 Jan 2013 13:18:30 -0500 Subject: [PATCH 049/329] Working on transitions between states --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index aa759ea7dc..04f848f306 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -121,6 +121,16 @@ class CombinedOpenEndedModule(XModule): def update_task_states(self): self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() + current_task_state=json.loads(self.task_states[len(self.task_states)-1]) + if current_task_state['state']==self.DONE: + self.current_task_number+=1 + if self.current_task_number==len(self.task_xml): + self.state=self.DONE + self.current_task_number=self.current_task_number-1 + else: + self.state=self.INTERMEDIATE_DONE + self.setup_next_task() + def handle_ajax(self, dispatch, get): return_html = self.current_task.handle_ajax(dispatch,get, self.system) From 4ea9c738a352d7701d717545412d7c8de1312065 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 14:24:23 -0500 Subject: [PATCH 050/329] Fix state transitions --- .../xmodule/xmodule/combined_open_ended_module.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 04f848f306..50d1a7df0c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -114,12 +114,13 @@ class CombinedOpenEndedModule(XModule): return True def get_html(self): + self.update_task_states() html = self.current_task.get_html(self.system) return_html = rewrite_links(html, self.rewrite_content_links) - self.update_task_states() return return_html def update_task_states(self): + changed=False self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() current_task_state=json.loads(self.task_states[len(self.task_states)-1]) if current_task_state['state']==self.DONE: @@ -129,13 +130,19 @@ class CombinedOpenEndedModule(XModule): self.current_task_number=self.current_task_number-1 else: self.state=self.INTERMEDIATE_DONE + changed=True self.setup_next_task() + return changed + def update_task_states_ajax(self,return_html): + changed=self.update_task_states() + if changed(): + return_html=self.get_html() + return return_html def handle_ajax(self, dispatch, get): return_html = self.current_task.handle_ajax(dispatch,get, self.system) - self.update_task_states() - return return_html + return self.update_task_states_ajax(return_html) def get_instance_state(self): """ From bc6838f8dac0cf628095065920c625205bbba3b7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 16:25:39 -0500 Subject: [PATCH 051/329] Minor changes --- .../xmodule/combined_open_ended_module.py | 30 +++++++++++++++++-- .../js/src/combinedopenended/display.coffee | 0 .../xmodule/js/src/openended/display.coffee | 0 .../js/src/selfassessment/display.coffee | 2 -- lms/templates/self_assessment_prompt.html | 1 - 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee create mode 100644 common/lib/xmodule/xmodule/js/src/openended/display.coffee diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 50d1a7df0c..a314c89f01 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -42,7 +42,7 @@ class CombinedOpenEndedModule(XModule): DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} + js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee'), resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/openended/display.coffee')]} js_module_name = "SelfAssessment" def __init__(self, system, location, definition, descriptor, @@ -92,6 +92,7 @@ class CombinedOpenEndedModule(XModule): tag=etree.fromstring(xml).tag return tag + def setup_next_task(self): current_task_state=None if self.state in [self.ASSESSING, self.DONE]: @@ -104,10 +105,18 @@ class CombinedOpenEndedModule(XModule): if current_task_type=="selfassessment": self.current_task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) - if current_task_state is None: + if current_task_state is None and self.current_task_number==0: self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING + elif current_task_state is None and self.current_task_number>0: + last_response=self.get_last_response(self.current_task_number-1) + current_task_state = ('{"state": "assessing", "version": 1, "max_score": {max_score}, ' + '"attempts": 0, "history": [{"answer": "{answer}"}]}' + .format(max_score=self._max_score, answer=last_response)) + self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING else: self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) @@ -119,6 +128,21 @@ class CombinedOpenEndedModule(XModule): return_html = rewrite_links(html, self.rewrite_content_links) return return_html + def get_last_response(self, task_number): + last_response="" + task_state = self.task_states[task_number] + task_xml=self.task_xml[task_number] + task_type=self.get_tag_name(task_xml) + + if task_type=="selfassessment": + task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) + task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) + task=self_assessment_module.SelfAssessmentModule(self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) + last_response=task.latest_answer() + last_score = task.latest_score() + + return last_response, last_score + def update_task_states(self): changed=False self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() @@ -136,7 +160,7 @@ class CombinedOpenEndedModule(XModule): def update_task_states_ajax(self,return_html): changed=self.update_task_states() - if changed(): + if changed: return_html=self.get_html() return return_html diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/lib/xmodule/xmodule/js/src/openended/display.coffee b/common/lib/xmodule/xmodule/js/src/openended/display.coffee new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee index 5b70ab29aa..4bf9cc8180 100644 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -15,8 +15,6 @@ class @SelfAssessment @hint_wrapper = @$('.hint-wrapper') @message_wrapper = @$('.message-wrapper') @submit_button = @$('.submit-button') - @reset_button = @$('.reset-button') - @reset_button.click @reset @find_assessment_elements() @find_hint_elements() diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 91472cbdaf..e54864c988 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -16,5 +16,4 @@
${initial_message}
- From cac5b75cf4b6e570b9d94ba20480bb494a9b5d31 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 16:36:18 -0500 Subject: [PATCH 052/329] Change modules --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a314c89f01..46f912a8de 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -42,7 +42,8 @@ class CombinedOpenEndedModule(XModule): DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee'), resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/openended/display.coffee')]} + #, resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/openended/display.coffee') + js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} js_module_name = "SelfAssessment" def __init__(self, system, location, definition, descriptor, From 8e03c8f6557c2c50db18743dc82b5986c47ce396 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 16:50:47 -0500 Subject: [PATCH 053/329] Fix missing comma issue --- common/lib/xmodule/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index c20b50ec66..c867fca228 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -19,7 +19,7 @@ setup( "abtest = xmodule.abtest_module:ABTestDescriptor", "book = xmodule.backcompat_module:TranslateCustomTagDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor", - "combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor" + "combinedopenended = xmodule.combined_open_ended_module:CombinedOpenEndedDescriptor", "course = xmodule.course_module:CourseDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor", "discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor", From 8ca12ce50023cf1f1525b100465cf43fcfdb59d8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 17:12:58 -0500 Subject: [PATCH 054/329] Make combined open ended a container --- .../xmodule/combined_open_ended_module.py | 14 +++++++++++ .../combinedopenended.coffee | 23 +++++++++++++++++++ .../js/src/combinedopenended/display.coffee | 0 .../xmodule/js/src/openended/display.coffee | 0 .../lib/xmodule/xmodule/open_ended_module.py | 1 - lms/templates/combined_open_ended.html | 9 ++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee delete mode 100644 common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee delete mode 100644 common/lib/xmodule/xmodule/js/src/openended/display.coffee delete mode 100644 common/lib/xmodule/xmodule/open_ended_module.py create mode 100644 lms/templates/combined_open_ended.html diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 46f912a8de..5eec8f34fd 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -124,6 +124,20 @@ class CombinedOpenEndedModule(XModule): return True def get_html(self): + task_html=self.get_html_base() + #set context variables and render template + + context = { + 'items': [{'content' : task_html}], + 'ajax_url': self.system.ajax_url, + 'allow_reset': True, + } + + html = system.render_template('combined_open_ended.html', context) + return html + + + def get_html_base(self): self.update_task_states() html = self.current_task.get_html(self.system) return_html = rewrite_links(html, self.rewrite_content_links) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee new file mode 100644 index 0000000000..6e286544a5 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee @@ -0,0 +1,23 @@ +class @CombinedOpenEnded + constructor: (element) -> + @el = $(element).find('section.combined-open-ended') + @ajax_url = @el.data('ajax-url') + @reset_button = @$('.reset-button') + @reset_button.click @reset + + reset: (event) => + event.preventDefault() + if @state == 'done' + $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => + if response.success + @answer_area.val('') + @rubric_wrapper.html('') + @hint_wrapper.html('') + @message_wrapper.html('') + @state = 'initial' + @rebind() + @reset_button.hide() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/lib/xmodule/xmodule/js/src/openended/display.coffee b/common/lib/xmodule/xmodule/js/src/openended/display.coffee deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py deleted file mode 100644 index 8b13789179..0000000000 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html new file mode 100644 index 0000000000..3d4a69e0be --- /dev/null +++ b/lms/templates/combined_open_ended.html @@ -0,0 +1,9 @@ +
+ + % for item in items: +
${item['content'] | h}
+ % endfor + + +
+ From dc0a798941553082fc3c311b6793f2598f5b089e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 17:19:13 -0500 Subject: [PATCH 055/329] Javascript fixes to remove reset button from self assessment --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 6 +++--- .../{combinedopenended.coffee => display.coffee} | 0 .../xmodule/xmodule/js/src/selfassessment/display.coffee | 5 ----- lms/templates/combined_open_ended.html | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) rename common/lib/xmodule/xmodule/js/src/combinedopenended/{combinedopenended.coffee => display.coffee} (100%) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 5eec8f34fd..82086d6b54 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -42,8 +42,8 @@ class CombinedOpenEndedModule(XModule): DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] - #, resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/openended/display.coffee') - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} + #, resource_string(__name__, 'js/src/openended/display.coffee') + js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee'), resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} js_module_name = "SelfAssessment" def __init__(self, system, location, definition, descriptor, @@ -133,7 +133,7 @@ class CombinedOpenEndedModule(XModule): 'allow_reset': True, } - html = system.render_template('combined_open_ended.html', context) + html = self.system.render_template('combined_open_ended.html', context) return html diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee similarity index 100% rename from common/lib/xmodule/xmodule/js/src/combinedopenended/combinedopenended.coffee rename to common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee index 4bf9cc8180..ea1f02c101 100644 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -29,7 +29,6 @@ class @SelfAssessment # rebind to the appropriate function for the current state @submit_button.unbind('click') @submit_button.show() - @reset_button.hide() @hint_area.attr('disabled', false) if @state == 'initial' @answer_area.attr("disabled", false) @@ -47,10 +46,6 @@ class @SelfAssessment @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() - if @allow_reset - @reset_button.show() - else - @reset_button.hide() find_assessment_elements: -> diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 3d4a69e0be..76c28b0e31 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,7 +1,7 @@
% for item in items: -
${item['content'] | h}
+
${item['content'] | n}
% endfor From 36c55d347bbc917f2ce787a9a541becb212fbb81 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 17:27:15 -0500 Subject: [PATCH 056/329] Combined JS --- .../xmodule/combined_open_ended_module.py | 4 +- .../js/src/combinedopenended/display.coffee | 137 ++++++++++++++++-- .../js/src/selfassessment/display.coffee | 123 ---------------- 3 files changed, 123 insertions(+), 141 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 82086d6b54..e77cd254e2 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -43,8 +43,8 @@ class CombinedOpenEndedModule(XModule): TASK_TYPES=["self", "ml", "instructor", "peer"] #, resource_string(__name__, 'js/src/openended/display.coffee') - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee'), resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} - js_module_name = "SelfAssessment" + js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} + js_module_name = "CombinedOpenEnded" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 6e286544a5..897d5f1b67 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -1,23 +1,128 @@ class @CombinedOpenEnded constructor: (element) -> - @el = $(element).find('section.combined-open-ended') + @el = $(element).find('section.self-assessment') + @id = @el.data('id') @ajax_url = @el.data('ajax-url') + @state = @el.data('state') + @allow_reset = @el.data('allow_reset') + # valid states: 'initial', 'assessing', 'request_hint', 'done' + + # Where to put the rubric once we load it + @errors_area = @$('.error') + @answer_area = @$('textarea.answer') + + @rubric_wrapper = @$('.rubric-wrapper') + @hint_wrapper = @$('.hint-wrapper') + @message_wrapper = @$('.message-wrapper') + @submit_button = @$('.submit-button') + @reset_button = @$('.reset-button') @reset_button.click @reset + @find_assessment_elements() + @find_hint_elements() + + @rebind() + + # locally scoped jquery. + $: (selector) -> + $(selector, @el) + + rebind: () => + # rebind to the appropriate function for the current state + @submit_button.unbind('click') + @submit_button.show() + @hint_area.attr('disabled', false) + if @state == 'initial' + @answer_area.attr("disabled", false) + @submit_button.prop('value', 'Submit') + @submit_button.click @save_answer + else if @state == 'assessing' + @answer_area.attr("disabled", true) + @submit_button.prop('value', 'Submit assessment') + @submit_button.click @save_assessment + else if @state == 'request_hint' + @answer_area.attr("disabled", true) + @submit_button.prop('value', 'Submit hint') + @submit_button.click @save_hint + else if @state == 'done' + @answer_area.attr("disabled", true) + @hint_area.attr('disabled', true) + @submit_button.hide() + + find_assessment_elements: -> + @assessment = @$('select.assessment') + + find_hint_elements: -> + @hint_area = @$('textarea.hint') + + save_answer: (event) => + event.preventDefault() + if @state == 'initial' + data = {'student_answer' : @answer_area.val()} + $.postWithPrefix "#{@ajax_url}/save_answer", data, (response) => + if response.success + @rubric_wrapper.html(response.rubric_html) + @state = 'assessing' + @find_assessment_elements() + @rebind() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') + + save_assessment: (event) => + event.preventDefault() + if @state == 'assessing' + data = {'assessment' : @assessment.find(':selected').text()} + $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => + if response.success + @state = response.state + + if @state == 'request_hint' + @hint_wrapper.html(response.hint_html) + @find_hint_elements() + else if @state == 'done' + @message_wrapper.html(response.message_html) + @allow_reset = response.allow_reset + + @rebind() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') + + + save_hint: (event) => + event.preventDefault() + if @state == 'request_hint' + data = {'hint' : @hint_area.val()} + + $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) => + if response.success + @message_wrapper.html(response.message_html) + @state = 'done' + @allow_reset = response.allow_reset + @rebind() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') + + reset: (event) => - event.preventDefault() - if @state == 'done' - $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => - if response.success - @answer_area.val('') - @rubric_wrapper.html('') - @hint_wrapper.html('') - @message_wrapper.html('') - @state = 'initial' - @rebind() - @reset_button.hide() - else - @errors_area.html(response.error) - else - @errors_area.html('Problem state got out of sync. Try reloading the page.') \ No newline at end of file + event.preventDefault() + if @state == 'done' + $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => + if response.success + @answer_area.val('') + @rubric_wrapper.html('') + @hint_wrapper.html('') + @message_wrapper.html('') + @state = 'initial' + @rebind() + @reset_button.hide() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee index ea1f02c101..30024478b1 100644 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -1,126 +1,3 @@ class @SelfAssessment constructor: (element) -> - @el = $(element).find('section.self-assessment') - @id = @el.data('id') - @ajax_url = @el.data('ajax-url') - @state = @el.data('state') - @allow_reset = @el.data('allow_reset') - # valid states: 'initial', 'assessing', 'request_hint', 'done' - # Where to put the rubric once we load it - @errors_area = @$('.error') - @answer_area = @$('textarea.answer') - - @rubric_wrapper = @$('.rubric-wrapper') - @hint_wrapper = @$('.hint-wrapper') - @message_wrapper = @$('.message-wrapper') - @submit_button = @$('.submit-button') - - @find_assessment_elements() - @find_hint_elements() - - @rebind() - - # locally scoped jquery. - $: (selector) -> - $(selector, @el) - - rebind: () => - # rebind to the appropriate function for the current state - @submit_button.unbind('click') - @submit_button.show() - @hint_area.attr('disabled', false) - if @state == 'initial' - @answer_area.attr("disabled", false) - @submit_button.prop('value', 'Submit') - @submit_button.click @save_answer - else if @state == 'assessing' - @answer_area.attr("disabled", true) - @submit_button.prop('value', 'Submit assessment') - @submit_button.click @save_assessment - else if @state == 'request_hint' - @answer_area.attr("disabled", true) - @submit_button.prop('value', 'Submit hint') - @submit_button.click @save_hint - else if @state == 'done' - @answer_area.attr("disabled", true) - @hint_area.attr('disabled', true) - @submit_button.hide() - - - find_assessment_elements: -> - @assessment = @$('select.assessment') - - find_hint_elements: -> - @hint_area = @$('textarea.hint') - - save_answer: (event) => - event.preventDefault() - if @state == 'initial' - data = {'student_answer' : @answer_area.val()} - $.postWithPrefix "#{@ajax_url}/save_answer", data, (response) => - if response.success - @rubric_wrapper.html(response.rubric_html) - @state = 'assessing' - @find_assessment_elements() - @rebind() - else - @errors_area.html(response.error) - else - @errors_area.html('Problem state got out of sync. Try reloading the page.') - - save_assessment: (event) => - event.preventDefault() - if @state == 'assessing' - data = {'assessment' : @assessment.find(':selected').text()} - $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => - if response.success - @state = response.state - - if @state == 'request_hint' - @hint_wrapper.html(response.hint_html) - @find_hint_elements() - else if @state == 'done' - @message_wrapper.html(response.message_html) - @allow_reset = response.allow_reset - - @rebind() - else - @errors_area.html(response.error) - else - @errors_area.html('Problem state got out of sync. Try reloading the page.') - - - save_hint: (event) => - event.preventDefault() - if @state == 'request_hint' - data = {'hint' : @hint_area.val()} - - $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) => - if response.success - @message_wrapper.html(response.message_html) - @state = 'done' - @allow_reset = response.allow_reset - @rebind() - else - @errors_area.html(response.error) - else - @errors_area.html('Problem state got out of sync. Try reloading the page.') - - - reset: (event) => - event.preventDefault() - if @state == 'done' - $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => - if response.success - @answer_area.val('') - @rubric_wrapper.html('') - @hint_wrapper.html('') - @message_wrapper.html('') - @state = 'initial' - @rebind() - @reset_button.hide() - else - @errors_area.html(response.error) - else - @errors_area.html('Problem state got out of sync. Try reloading the page.') From b8d49511e1415d51270befef773e8f273c9f732b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 18:14:43 -0500 Subject: [PATCH 057/329] Fixing javascript display bugs --- .../xmodule/combined_open_ended_module.py | 5 +-- .../js/src/combinedopenended/display.coffee | 39 +++++++++++-------- .../js/src/selfassessment/display.coffee | 3 -- .../xmodule/xmodule/self_assessment_module.py | 3 -- lms/templates/combined_open_ended.html | 4 +- lms/templates/self_assessment_prompt.html | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index e77cd254e2..c75de86b02 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -112,9 +112,8 @@ class CombinedOpenEndedModule(XModule): self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: last_response=self.get_last_response(self.current_task_number-1) - current_task_state = ('{"state": "assessing", "version": 1, "max_score": {max_score}, ' - '"attempts": 0, "history": [{"answer": "{answer}"}]}' - .format(max_score=self._max_score, answer=last_response)) + current_task_state = ("{'state': 'assessing', 'version': 1, 'max_score': {" + str(self._max_score) + "}, " + + "'attempts': 0, 'history': [{'answer': '{" + str(last_response) + "}'}]}") self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 897d5f1b67..ebb440b996 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -1,13 +1,16 @@ class @CombinedOpenEnded constructor: (element) -> - @el = $(element).find('section.self-assessment') + @el = $(element).find('section.combined-open-ended') @id = @el.data('id') @ajax_url = @el.data('ajax-url') @state = @el.data('state') @allow_reset = @el.data('allow_reset') + @reset_button = @$('.reset-button') + @reset_button.click @reset # valid states: 'initial', 'assessing', 'request_hint', 'done' # Where to put the rubric once we load it + @el = $(element).find('section.open-ended-child') @errors_area = @$('.error') @answer_area = @$('textarea.answer') @@ -15,9 +18,7 @@ class @CombinedOpenEnded @hint_wrapper = @$('.hint-wrapper') @message_wrapper = @$('.message-wrapper') @submit_button = @$('.submit-button') - - @reset_button = @$('.reset-button') - @reset_button.click @reset + @child_state = @el.data('state') @find_assessment_elements() @find_hint_elements() @@ -32,23 +33,28 @@ class @CombinedOpenEnded # rebind to the appropriate function for the current state @submit_button.unbind('click') @submit_button.show() + @reset_button.hide() @hint_area.attr('disabled', false) - if @state == 'initial' + if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') @submit_button.click @save_answer - else if @state == 'assessing' + else if @child_state == 'assessing' @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit assessment') @submit_button.click @save_assessment - else if @state == 'request_hint' + else if @child_state == 'request_hint' @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit hint') @submit_button.click @save_hint - else if @state == 'done' + else if @child_state == 'done' @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() + if @allow_reset + @reset_button.show() + else + @reset_button.hide() find_assessment_elements: -> @assessment = @$('select.assessment') @@ -58,12 +64,12 @@ class @CombinedOpenEnded save_answer: (event) => event.preventDefault() - if @state == 'initial' + if @child_state == 'initial' data = {'student_answer' : @answer_area.val()} $.postWithPrefix "#{@ajax_url}/save_answer", data, (response) => if response.success @rubric_wrapper.html(response.rubric_html) - @state = 'assessing' + @child_state = 'assessing' @find_assessment_elements() @rebind() else @@ -73,16 +79,16 @@ class @CombinedOpenEnded save_assessment: (event) => event.preventDefault() - if @state == 'assessing' + if @child_state == 'assessing' data = {'assessment' : @assessment.find(':selected').text()} $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => if response.success - @state = response.state + @child_state = response.state - if @state == 'request_hint' + if @child_state == 'request_hint' @hint_wrapper.html(response.hint_html) @find_hint_elements() - else if @state == 'done' + else if @child_state == 'done' @message_wrapper.html(response.message_html) @allow_reset = response.allow_reset @@ -95,13 +101,13 @@ class @CombinedOpenEnded save_hint: (event) => event.preventDefault() - if @state == 'request_hint' + if @child_state == 'request_hint' data = {'hint' : @hint_area.val()} $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) => if response.success @message_wrapper.html(response.message_html) - @state = 'done' + @child_state = 'done' @allow_reset = response.allow_reset @rebind() else @@ -112,6 +118,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() + @errors_area.html('Problem state got out of sync. Try reloading the page.') if @state == 'done' $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee deleted file mode 100644 index 30024478b1..0000000000 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ /dev/null @@ -1,3 +0,0 @@ -class @SelfAssessment - constructor: (element) -> - diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 0c37384c16..a3b4b56e35 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -61,9 +61,6 @@ class SelfAssessmentModule(): REQUEST_HINT = 'request_hint' DONE = 'done' - js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} - js_module_name = "SelfAssessment" - def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): """ diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 76c28b0e31..65a57b98eb 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,9 +1,9 @@ -
+
% for item in items:
${item['content'] | n}
% endfor -
+ diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index e54864c988..91223fdaa1 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -1,4 +1,4 @@ -
From beef00c13a4c30c04a0335b09935f0ea8b2121ae Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 18:31:19 -0500 Subject: [PATCH 058/329] Json instance state fix --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index c75de86b02..14e04d399e 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -111,9 +111,10 @@ class CombinedOpenEndedModule(XModule): self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: - last_response=self.get_last_response(self.current_task_number-1) - current_task_state = ("{'state': 'assessing', 'version': 1, 'max_score': {" + str(self._max_score) + "}, " + - "'attempts': 0, 'history': [{'answer': '{" + str(last_response) + "}'}]}") + last_response, last_score=self.get_last_response(self.current_task_number-1) + current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "history": [{"answer": "' + str(last_response) + '"}]}') + {"state": "done", "version": 1, "max_score": 1, "attempts": 1, "history": [{"answer": "gdgddg", "score": 0, "hint": "dfdfdf"}]} self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING From 418c45f4d39a311bd8678e9b21b017f998c14896 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 18:34:33 -0500 Subject: [PATCH 059/329] Bugfixes --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index ebb440b996..1636804e71 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -119,7 +119,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() @errors_area.html('Problem state got out of sync. Try reloading the page.') - if @state == 'done' + if @child_state == 'done' $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success @answer_area.val('') From 5d6509dcb5933ff4fac2dbc944be2432a606d058 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 18:46:48 -0500 Subject: [PATCH 060/329] add in js variables for element wrappers --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 1636804e71..0c54e9af46 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -7,6 +7,7 @@ class @CombinedOpenEnded @allow_reset = @el.data('allow_reset') @reset_button = @$('.reset-button') @reset_button.click @reset + @combined_open_ended= @$('.combined-open-ended') # valid states: 'initial', 'assessing', 'request_hint', 'done' # Where to put the rubric once we load it @@ -20,6 +21,8 @@ class @CombinedOpenEnded @submit_button = @$('.submit-button') @child_state = @el.data('state') + @open_ended_child= @$('.open-ended-child') + @find_assessment_elements() @find_hint_elements() From 406d6c0c85e776b9c975c4ec772909129439a5a4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 3 Jan 2013 18:49:27 -0500 Subject: [PATCH 061/329] js name changes --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 0c54e9af46..031cc5183b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -129,7 +129,7 @@ class @CombinedOpenEnded @rubric_wrapper.html('') @hint_wrapper.html('') @message_wrapper.html('') - @state = 'initial' + @child_state = 'initial' @rebind() @reset_button.hide() else From d5c3b58da394e068cc73af42204be5b7f4f749cd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 09:56:45 -0500 Subject: [PATCH 062/329] Change reset and next problem code --- .../xmodule/combined_open_ended_module.py | 29 +++++++++++++++++-- .../js/src/combinedopenended/display.coffee | 26 ++++++++++++++--- .../xmodule/xmodule/self_assessment_module.py | 2 +- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 14e04d399e..58d0cd4560 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -176,12 +176,35 @@ class CombinedOpenEndedModule(XModule): def update_task_states_ajax(self,return_html): changed=self.update_task_states() if changed: - return_html=self.get_html() + #return_html=self.get_html() + pass return return_html def handle_ajax(self, dispatch, get): - return_html = self.current_task.handle_ajax(dispatch,get, self.system) - return self.update_task_states_ajax(return_html) + """ + This is called by courseware.module_render, to handle an AJAX call. + "get" is request.POST. + + Returns a json dictionary: + { 'progress_changed' : True/False, + 'progress': 'none'/'in_progress'/'done', + } + """ + + handlers = { + 'next_problem': self.next_problem, + 'reset': self.reset, + } + + if dispatch not in handlers: + return_html = self.current_task.handle_ajax(dispatch,get, self.system) + return self.update_task_states_ajax(return_html) + + def next_problem(self): + pass + + def reset(self): + pass def get_instance_state(self): """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 031cc5183b..a7aee1b895 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -8,7 +8,7 @@ class @CombinedOpenEnded @reset_button = @$('.reset-button') @reset_button.click @reset @combined_open_ended= @$('.combined-open-ended') - # valid states: 'initial', 'assessing', 'request_hint', 'done' + # valid states: 'initial', 'assessing', 'post_assessment', 'done' # Where to put the rubric once we load it @el = $(element).find('section.open-ended-child') @@ -46,7 +46,7 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit assessment') @submit_button.click @save_assessment - else if @child_state == 'request_hint' + else if @child_state == 'post_assessment' @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit hint') @submit_button.click @save_hint @@ -88,7 +88,7 @@ class @CombinedOpenEnded if response.success @child_state = response.state - if @child_state == 'request_hint' + if @child_state == 'post_assessment' @hint_wrapper.html(response.hint_html) @find_hint_elements() else if @child_state == 'done' @@ -104,7 +104,7 @@ class @CombinedOpenEnded save_hint: (event) => event.preventDefault() - if @child_state == 'request_hint' + if @child_state == 'post_assessment' data = {'hint' : @hint_area.val()} $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) => @@ -134,5 +134,23 @@ class @CombinedOpenEnded @reset_button.hide() else @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') + + next_problem (event) => + event.preventDefault() + @errors_area.html('Problem state got out of sync. Try reloading the page.') + if @child_state == 'done' + $.postWithPrefix "#{@ajax_url}/next_problem", {}, (response) => + if response.success + @answer_area.val('') + @rubric_wrapper.html('') + @hint_wrapper.html('') + @message_wrapper.html('') + @child_state = 'initial' + @rebind() + @reset_button.hide() + else + @errors_area.html(response.error) else @errors_area.html('Problem state got out of sync. Try reloading the page.') \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index a3b4b56e35..cba13c7e54 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -58,7 +58,7 @@ class SelfAssessmentModule(): # states INITIAL = 'initial' ASSESSING = 'assessing' - REQUEST_HINT = 'request_hint' + REQUEST_HINT = 'post_assessment' DONE = 'done' def __init__(self, system, location, definition, descriptor, From 91a9962bef454d5ef626d3680a515a42a88a498b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 09:59:25 -0500 Subject: [PATCH 063/329] Improve next problem handling and state tracking --- .../xmodule/js/src/combinedopenended/display.coffee | 13 +++++++++---- lms/templates/combined_open_ended.html | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a7aee1b895..0e8cadd69f 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -7,6 +7,8 @@ class @CombinedOpenEnded @allow_reset = @el.data('allow_reset') @reset_button = @$('.reset-button') @reset_button.click @reset + @next_problem_button = @$('.next-step-button') + @next_problem_button.click @next_problem @combined_open_ended= @$('.combined-open-ended') # valid states: 'initial', 'assessing', 'post_assessment', 'done' @@ -37,6 +39,7 @@ class @CombinedOpenEnded @submit_button.unbind('click') @submit_button.show() @reset_button.hide() + @next_problem_button.hide() @hint_area.attr('disabled', false) if @child_state == 'initial' @answer_area.attr("disabled", false) @@ -54,10 +57,12 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() - if @allow_reset - @reset_button.show() - else - @reset_button.hide() + if !@state == 'done' + @next_problem_button.show() + if @allow_reset + @reset_button.show() + else + @reset_button.hide() find_assessment_elements: -> @assessment = @$('select.assessment') diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 65a57b98eb..51afddcd15 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -5,5 +5,6 @@ % endfor +
From 6ef8713c9406c850c2da21b120cdf822bdb77e4d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 10:55:05 -0500 Subject: [PATCH 064/329] JS state tracking --- .../xmodule/combined_open_ended_module.py | 27 ++++++++++++++++--- .../js/src/combinedopenended/display.coffee | 5 ++-- .../xmodule/xmodule/self_assessment_module.py | 2 +- lms/templates/combined_open_ended.html | 2 +- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 58d0cd4560..a57bf965d0 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -131,6 +131,7 @@ class CombinedOpenEndedModule(XModule): 'items': [{'content' : task_html}], 'ajax_url': self.system.ajax_url, 'allow_reset': True, + 'state' : self.state, } html = self.system.render_template('combined_open_ended.html', context) @@ -201,12 +202,32 @@ class CombinedOpenEndedModule(XModule): return self.update_task_states_ajax(return_html) def next_problem(self): - pass + self.setup_next_task() + return {'success' : True} def reset(self): - pass + """ + If resetting is allowed, reset the state. - def get_instance_state(self): + Returns {'success': bool, 'error': msg} + (error only present if not success) + """ + if self.state != self.DONE: + return self.out_of_sync_error(get) + + if self.attempts > self.max_attempts: + return { + 'success': False, + 'error': 'Too many attempts.' + } + self.state=self.INITIAL + self.current_task_number=0 + self.setup_next_task() + self.current_task.reset(self.system) + return {'success': True} + + +def get_instance_state(self): """ Get the current score and state """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 0e8cadd69f..eab904b0a4 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -59,6 +59,7 @@ class @CombinedOpenEnded @submit_button.hide() if !@state == 'done' @next_problem_button.show() + if @state == 'done' if @allow_reset @reset_button.show() else @@ -127,7 +128,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() @errors_area.html('Problem state got out of sync. Try reloading the page.') - if @child_state == 'done' + if @state == 'done' $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success @answer_area.val('') @@ -142,7 +143,7 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - next_problem (event) => + next_problem: (event) => event.preventDefault() @errors_area.html('Problem state got out of sync. Try reloading the page.') if @child_state == 'done' diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index cba13c7e54..c1c568af2f 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -459,7 +459,7 @@ class SelfAssessmentModule(): 'allow_reset': self._allow_reset()} - def reset(self, get, system): + def reset(self, system): """ If resetting is allowed, reset the state. diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 51afddcd15..a00d5b042f 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,4 +1,4 @@ -
+
% for item in items:
${item['content'] | n}
From 70d0e6a1b601422f4c24e1420924c0a77e4b0455 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:11:35 -0500 Subject: [PATCH 065/329] Silly tab error --- .../xmodule/xmodule/combined_open_ended_module.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a57bf965d0..a73886c7e2 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -201,11 +201,14 @@ class CombinedOpenEndedModule(XModule): return_html = self.current_task.handle_ajax(dispatch,get, self.system) return self.update_task_states_ajax(return_html) - def next_problem(self): + d = handlers[dispatch](get) + return json.dumps(d,cls=ComplexEncoder) + + def next_problem(self, get): self.setup_next_task() return {'success' : True} - def reset(self): + def reset(self, get): """ If resetting is allowed, reset the state. @@ -221,13 +224,15 @@ class CombinedOpenEndedModule(XModule): 'error': 'Too many attempts.' } self.state=self.INITIAL + for i in xrange(0,len(self.task_xml)): + self.current_task_number=i + self.setup_next_task() + self.current_task.reset(self.system) self.current_task_number=0 - self.setup_next_task() - self.current_task.reset(self.system) return {'success': True} -def get_instance_state(self): + def get_instance_state(self): """ Get the current score and state """ From dabc2e0c9d33b0346c348834d003a0fa339e31c8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:21:08 -0500 Subject: [PATCH 066/329] Javascript state transitions --- .../js/src/combinedopenended/display.coffee | 6 +++++- .../lib/xmodule/xmodule/self_assessment_module.py | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index eab904b0a4..8e1dff4d99 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -57,8 +57,10 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() - if !@state == 'done' + if @state != 'done' @next_problem_button.show() + else + @next_problem_button.hide() if @state == 'done' if @allow_reset @reset_button.show() @@ -138,6 +140,7 @@ class @CombinedOpenEnded @child_state = 'initial' @rebind() @reset_button.hide() + location.reload() else @errors_area.html(response.error) else @@ -156,6 +159,7 @@ class @CombinedOpenEnded @child_state = 'initial' @rebind() @reset_button.hide() + location.reload() else @errors_area.html(response.error) else diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index c1c568af2f..64cf140d38 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -466,14 +466,14 @@ class SelfAssessmentModule(): Returns {'success': bool, 'error': msg} (error only present if not success) """ - if self.state != self.DONE: - return self.out_of_sync_error(get) + #if self.state != self.DONE: + # return self.out_of_sync_error(get) - if self.attempts > self.max_attempts: - return { - 'success': False, - 'error': 'Too many attempts.' - } + #if self.attempts > self.max_attempts: + # return { + # 'success': False, + # 'error': 'Too many attempts.' + # } self.change_state(self.INITIAL) return {'success': True} From d910395c7de4d07b5b1401bf05918ab036eda554 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:24:05 -0500 Subject: [PATCH 067/329] Task count tracking --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 ++ lms/templates/combined_open_ended.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a73886c7e2..afa5f61b25 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -132,6 +132,8 @@ class CombinedOpenEndedModule(XModule): 'ajax_url': self.system.ajax_url, 'allow_reset': True, 'state' : self.state, + 'task_count' : len(self.task_xml), + 'task_number' : self.current_task_number, } html = self.system.render_template('combined_open_ended.html', context) diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index a00d5b042f..5b6823e809 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,4 +1,4 @@ -
+
% for item in items:
${item['content'] | n}
From 69d8c4a97719154801d79322b84cfe985255243a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:27:35 -0500 Subject: [PATCH 068/329] javascript state tracking for next problem and reset --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- .../xmodule/js/src/combinedopenended/display.coffee | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index afa5f61b25..f33439673c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -133,7 +133,7 @@ class CombinedOpenEndedModule(XModule): 'allow_reset': True, 'state' : self.state, 'task_count' : len(self.task_xml), - 'task_number' : self.current_task_number, + 'task_number' : self.current_task_number+1, } html = self.system.render_template('combined_open_ended.html', context) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 8e1dff4d99..34b9b0c472 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -4,6 +4,9 @@ class @CombinedOpenEnded @id = @el.data('id') @ajax_url = @el.data('ajax-url') @state = @el.data('state') + @task_count = @el.data('task-count') + @task_number = @el.data('task-number') + @allow_reset = @el.data('allow_reset') @reset_button = @$('.reset-button') @reset_button.click @reset @@ -57,16 +60,16 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() - if @state != 'done' + if @task_number<@task_count @next_problem_button.show() else @next_problem_button.hide() - if @state == 'done' if @allow_reset @reset_button.show() else @reset_button.hide() + find_assessment_elements: -> @assessment = @$('select.assessment') From 844fb03377aa9f70789e261184f1282bcfcf8a3b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:35:27 -0500 Subject: [PATCH 069/329] javascript transitions okay, can integrate open ended response now --- .../js/src/combinedopenended/display.coffee | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 34b9b0c472..08c7aafa4d 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -64,10 +64,10 @@ class @CombinedOpenEnded @next_problem_button.show() else @next_problem_button.hide() - if @allow_reset - @reset_button.show() - else - @reset_button.hide() + #if @allow_reset + @reset_button.show() + #else + # @reset_button.hide() find_assessment_elements: -> @@ -132,8 +132,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() - @errors_area.html('Problem state got out of sync. Try reloading the page.') - if @state == 'done' + if @child_state == 'done' $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success @answer_area.val('') @@ -151,7 +150,6 @@ class @CombinedOpenEnded next_problem: (event) => event.preventDefault() - @errors_area.html('Problem state got out of sync. Try reloading the page.') if @child_state == 'done' $.postWithPrefix "#{@ajax_url}/next_problem", {}, (response) => if response.success @@ -161,7 +159,7 @@ class @CombinedOpenEnded @message_wrapper.html('') @child_state = 'initial' @rebind() - @reset_button.hide() + @next_problem_button.hide() location.reload() else @errors_area.html(response.error) From e3a311d28347a9bd128a26281b914265c93801e6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:40:49 -0500 Subject: [PATCH 070/329] Start creating open ended xmodule --- common/lib/xmodule/open_ended_module.py | 128 ++++++++++++++++++ .../xmodule/xmodule/self_assessment_module.py | 9 +- 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 common/lib/xmodule/open_ended_module.py diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py new file mode 100644 index 0000000000..e728a8dba3 --- /dev/null +++ b/common/lib/xmodule/open_ended_module.py @@ -0,0 +1,128 @@ +""" +A Self Assessment module that allows students to write open-ended responses, +submit, then see a rubric and rate themselves. Persists student supplied +hints, answers, and assessment judgment (currently only correct/incorrect). +Parses xml definition file--see below for exact format. +""" + +import copy +from fs.errors import ResourceNotFoundError +import itertools +import json +import logging +from lxml import etree +from lxml.html import rewrite_links +from path import path +import os +import sys + +from pkg_resources import resource_string + +from .capa_module import only_one, ComplexEncoder +from .editing_module import EditingDescriptor +from .html_checker import check_html +from progress import Progress +from .stringify import stringify_children +from .x_module import XModule +from .xml_module import XmlDescriptor +from xmodule.modulestore import Location + +log = logging.getLogger("mitx.courseware") + +# Set the default number of max attempts. Should be 1 for production +# Set higher for debugging/testing +# attempts specified in xml definition overrides this. +MAX_ATTEMPTS = 1 + +# Set maximum available number of points. +# Overriden by max_score specified in xml. +MAX_SCORE = 1 + +class OpenEndedModule(): + """ + States: + + initial (prompt, textbox shown) + | + assessing (read-only textbox, rubric + assessment input shown) + | + request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) + | + done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows + a reset button that goes back to initial state. Saves previous + submissions too.) + """ + + DEFAULT_QUEUE = 'open-ended' + DEFAULT_MESSAGE_QUEUE = 'open-ended-message' + response_tag = 'openendedresponse' + allowed_inputfields = ['openendedinput'] + max_inputfields = 1 + + STATE_VERSION = 1 + + # states + INITIAL = 'initial' + ASSESSING = 'assessing' + REQUEST_HINT = 'post_assessment' + DONE = 'done' + + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + """ + Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, + and two optional attributes: + attempts, which should be an integer that defaults to 1. + If it's > 1, the student will be able to re-submit after they see + the rubric. + max_score, which should be an integer that defaults to 1. + It defines the maximum number of points a student can get. Assumed to be integer scale + from 0 to max_score, with an interval of 1. + + Note: all the submissions are stored. + + Sample file: + + + + Insert prompt text here. (arbitrary html) + + + Insert grading rubric here. (arbitrary html) + + + Please enter a hint below: (arbitrary html) + + + Thanks for submitting! (arbitrary html) + + + """ + + # Load instance state + if instance_state is not None: + instance_state = json.loads(instance_state) + else: + instance_state = {} + + instance_state = self.convert_state_to_current_format(instance_state) + + # History is a list of tuples of (answer, score, hint), where hint may be + # None for any element, and score and hint can be None for the last (current) + # element. + # Scores are on scale from 0 to max_score + self.history = instance_state.get('history', []) + + self.state = instance_state.get('state', 'initial') + + self.attempts = instance_state.get('attempts', 0) + self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) + + # Used for progress / grading. Currently get credit just for + # completion (doesn't matter if you self-assessed correct/incorrect). + self._max_score = int(instance_state.get('max_score', MAX_SCORE)) + + self.rubric = definition['rubric'] + self.prompt = definition['prompt'] + self.submit_message = definition['submitmessage'] + self.hint_prompt = definition['hintprompt'] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 64cf140d38..0b7cc90994 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -93,15 +93,18 @@ class SelfAssessmentModule(): """ + self.xml = xml + self.inputfields = inputfields + self.context = context + self.system = system + # Load instance state if instance_state is not None: instance_state = json.loads(instance_state) else: instance_state = {} - instance_state = self.convert_state_to_current_format(instance_state) - - # History is a list of tuples of (answer, score, hint), where hint may be + # History is a list of tuples of (answer, score, feedback), where hint may be # None for any element, and score and hint can be None for the last (current) # element. # Scores are on scale from 0 to max_score From 6d61bd469a096eef5eca94cb88dbc3a5f13e9e2e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:47:49 -0500 Subject: [PATCH 071/329] initial setup for open ended xmodule --- common/lib/xmodule/open_ended_module.py | 59 +++++++++++++++++-- .../xmodule/xmodule/self_assessment_module.py | 5 -- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index e728a8dba3..62310f1e18 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -64,7 +64,7 @@ class OpenEndedModule(): # states INITIAL = 'initial' ASSESSING = 'assessing' - REQUEST_HINT = 'post_assessment' + POST_ASSESSMENT = 'post_assessment' DONE = 'done' def __init__(self, system, location, definition, descriptor, @@ -122,7 +122,56 @@ class OpenEndedModule(): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(instance_state.get('max_score', MAX_SCORE)) - self.rubric = definition['rubric'] - self.prompt = definition['prompt'] - self.submit_message = definition['submitmessage'] - self.hint_prompt = definition['hintprompt'] \ No newline at end of file + oeparam = definition['openendedparam'] + prompt = definition['prompt'] + rubric = definition['rubric'] + + self.url = definition.get('url', None) + self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE) + self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE) + + #This is needed to attach feedback to specific responses later + self.submission_id=None + self.grader_id=None + + if oeparam is None: + raise ValueError("No oeparam found in problem xml.") + if prompt is None: + raise ValueError("No prompt found in problem xml.") + if rubric is None: + raise ValueError("No rubric found in problem xml.") + + self._parse(oeparam, prompt, rubric) + + + def handle_ajax(self, dispatch, get): + ''' + This is called by courseware.module_render, to handle an AJAX call. + "get" is request.POST. + + Returns a json dictionary: + { 'progress_changed' : True/False, + 'progress' : 'none'/'in_progress'/'done', + } + ''' + handlers = { + 'problem_get': self.get_problem, + 'problem_check': self.check_problem, + 'problem_reset': self.reset_problem, + 'problem_save': self.save_problem, + 'problem_show': self.get_answer, + 'score_update': self.update_score, + 'message_post' : self.message_post, + } + + if dispatch not in handlers: + return 'Error' + + before = self.get_progress() + d = handlers[dispatch](get) + after = self.get_progress() + d.update({ + 'progress_changed': after != before, + 'progress_status': Progress.to_js_status_str(after), + }) + return json.dumps(d, cls=ComplexEncoder) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 0b7cc90994..1b10fab9ac 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -93,11 +93,6 @@ class SelfAssessmentModule(): """ - self.xml = xml - self.inputfields = inputfields - self.context = context - self.system = system - # Load instance state if instance_state is not None: instance_state = json.loads(instance_state) From 6ac90cf9544d89f3cf7da3f5a78066922af6a6fd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 11:53:33 -0500 Subject: [PATCH 072/329] Fix message post to work with ajax --- common/lib/xmodule/open_ended_module.py | 371 ++++++++++++++++++++++++ 1 file changed, 371 insertions(+) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index 62310f1e18..0ce2007c21 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -15,6 +15,8 @@ from lxml.html import rewrite_links from path import path import os import sys +import hashlib +import capa.xqueue_interface as xqueue_interface from pkg_resources import resource_string @@ -143,6 +145,375 @@ class OpenEndedModule(): self._parse(oeparam, prompt, rubric) + def _parse(self, oeparam, prompt, rubric): + ''' + Parse OpenEndedResponse XML: + self.initial_display + self.payload - dict containing keys -- + 'grader' : path to grader settings file, 'problem_id' : id of the problem + + self.answer - What to display when show answer is clicked + ''' + # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload + prompt_string = self.stringify_children(prompt) + rubric_string = self.stringify_children(rubric) + + grader_payload = oeparam.find('grader_payload') + grader_payload = grader_payload.text if grader_payload is not None else '' + + #Update grader payload with student id. If grader payload not json, error. + try: + parsed_grader_payload = json.loads(grader_payload) + # NOTE: self.system.location is valid because the capa_module + # __init__ adds it (easiest way to get problem location into + # response types) + except TypeError, ValueError: + log.exception("Grader payload %r is not a json object!", grader_payload) + + self.initial_display = find_with_default(oeparam, 'initial_display', '') + self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') + + parsed_grader_payload.update({ + 'location' : self.system.location, + 'course_id' : self.system.course_id, + 'prompt' : prompt_string, + 'rubric' : rubric_string, + 'initial_display' : self.initial_display, + 'answer' : self.answer, + }) + updated_grader_payload = json.dumps(parsed_grader_payload) + + self.payload = {'grader_payload': updated_grader_payload} + + try: + self.max_score = int(find_with_default(oeparam, 'max_score', 1)) + except ValueError: + self.max_score = 1 + + def handle_message_post(self,get): + """ + Handles a student message post (a reaction to the grade they received from an open ended grader type) + Returns a boolean success/fail and an error message + """ + + event_info = dict() + event_info['problem_id'] = self.location.url() + event_info['student_id'] = self.system.anonymous_student_id + event_info['survey_responses']= get + + survey_responses=event_info['survey_responses'] + for tag in ['feedback', 'submission_id', 'grader_id', 'score']: + if tag not in survey_responses: + return False, "Could not find needed tag {0}".format(tag) + try: + submission_id=int(survey_responses['submission_id']) + grader_id = int(survey_responses['grader_id']) + feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) + score = int(survey_responses['score']) + except: + error_message=("Could not parse submission id, grader id, " + "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) + log.exception(error_message) + return False, "There was an error saving your feedback. Please contact course staff." + + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) + anonymous_student_id = self.system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.answer_id) + + xheader = xqueue_interface.make_xheader( + lms_callback_url=self.system.xqueue['callback_url'], + lms_key=queuekey, + queue_name=self.message_queue_name + ) + + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime, + } + contents= { + 'feedback' : feedback, + 'submission_id' : submission_id, + 'grader_id' : grader_id, + 'score': score, + 'student_info' : json.dumps(student_info), + } + + (error, msg) = qinterface.send_to_queue(header=xheader, + body=json.dumps(contents)) + + #Convert error to a success value + success=True + if error: + success=False + + return success, "Successfully submitted your feedback." + + def get_score(self, student_answers): + + try: + submission = student_answers[self.answer_id] + except KeyError: + msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}' + .format(self.answer_id, student_answers)) + log.exception(msg) + raise LoncapaProblemError(msg) + + # Prepare xqueue request + #------------------------------------------------------------ + + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) + + anonymous_student_id = self.system.anonymous_student_id + + # Generate header + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.answer_id) + + xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], + lms_key=queuekey, + queue_name=self.queue_name) + + self.context.update({'submission': submission}) + + contents = self.payload.copy() + + # Metadata related to the student submission revealed to the external grader + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': qtime, + } + + #Update contents with student response and student info + contents.update({ + 'student_info': json.dumps(student_info), + 'student_response': submission, + 'max_score' : self.max_score, + }) + + # Submit request. When successful, 'msg' is the prior length of the queue + (error, msg) = qinterface.send_to_queue(header=xheader, + body=json.dumps(contents)) + + # State associated with the queueing request + queuestate = {'key': queuekey, + 'time': qtime,} + + cmap = CorrectMap() + if error: + cmap.set(self.answer_id, queuestate=None, + msg='Unable to deliver your submission to grader. (Reason: {0}.)' + ' Please try again later.'.format(msg)) + else: + # Queueing mechanism flags: + # 1) Backend: Non-null CorrectMap['queuestate'] indicates that + # the problem has been queued + # 2) Frontend: correctness='incomplete' eventually trickles down + # through inputtypes.textbox and .filesubmission to inform the + # browser that the submission is queued (and it could e.g. poll) + cmap.set(self.answer_id, queuestate=queuestate, + correctness='incomplete', msg=msg) + + return cmap + + def update_score(self, score_msg, oldcmap, queuekey): + log.debug(score_msg) + score_msg = self._parse_score_msg(score_msg) + if not score_msg.valid: + oldcmap.set(self.answer_id, + msg = 'Invalid grader reply. Please contact the course staff.') + return oldcmap + + correctness = 'correct' if score_msg.correct else 'incorrect' + + # TODO: Find out how this is used elsewhere, if any + self.context['correct'] = correctness + + # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # does not match, we keep waiting for the score_msg whose key actually matches + if oldcmap.is_right_queuekey(self.answer_id, queuekey): + # Sanity check on returned points + points = score_msg.points + if points < 0: + points = 0 + + # Queuestate is consumed, so reset it to None + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, + msg = score_msg.msg.replace(' ', ' '), queuestate=None) + else: + log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format( + queuekey, self.answer_id)) + + return oldcmap + + + def get_answers(self): + anshtml = '
{0}
'.format(self.answer) + return {self.answer_id: anshtml} + + def get_initial_display(self): + return {self.answer_id: self.initial_display} + + def _convert_longform_feedback_to_html(self, response_items): + """ + Take in a dictionary, and return html strings for display to student. + Input: + response_items: Dictionary with keys success, feedback. + if success is True, feedback should be a dictionary, with keys for + types of feedback, and the corresponding feedback values. + if success is False, feedback is actually an error string. + + NOTE: this will need to change when we integrate peer grading, because + that will have more complex feedback. + + Output: + String -- html that can be displayed to the student. + """ + + # We want to display available feedback in a particular order. + # This dictionary specifies which goes first--lower first. + priorities = {# These go at the start of the feedback + 'spelling': 0, + 'grammar': 1, + # needs to be after all the other feedback + 'markup_text': 3} + + default_priority = 2 + + def get_priority(elt): + """ + Args: + elt: a tuple of feedback-type, feedback + Returns: + the priority for this feedback type + """ + return priorities.get(elt[0], default_priority) + + def encode_values(feedback_type,value): + feedback_type=str(feedback_type).encode('ascii', 'ignore') + if not isinstance(value,basestring): + value=str(value) + value=value.encode('ascii', 'ignore') + return feedback_type,value + + def format_feedback(feedback_type, value): + feedback_type,value=encode_values(feedback_type,value) + feedback= """ +
+ {value} +
+ """.format(feedback_type=feedback_type, value=value) + return feedback + + def format_feedback_hidden(feedback_type , value): + feedback_type,value=encode_values(feedback_type,value) + feedback = """ + + """.format(feedback_type=feedback_type, value=value) + return feedback + + # TODO (vshnayder): design and document the details of this format so + # that we can do proper escaping here (e.g. are the graders allowed to + # include HTML?) + + for tag in ['success', 'feedback', 'submission_id', 'grader_id']: + if tag not in response_items: + return format_feedback('errors', 'Error getting feedback') + + feedback_items = response_items['feedback'] + try: + feedback = json.loads(feedback_items) + except (TypeError, ValueError): + log.exception("feedback_items have invalid json %r", feedback_items) + return format_feedback('errors', 'Could not parse feedback') + + if response_items['success']: + if len(feedback) == 0: + return format_feedback('errors', 'No feedback available') + + feedback_lst = sorted(feedback.items(), key=get_priority) + feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst) + else: + feedback_list_part1 = format_feedback('errors', response_items['feedback']) + + feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value) + for feedback_type,value in response_items.items() + if feedback_type in ['submission_id', 'grader_id']])) + + return u"\n".join([feedback_list_part1,feedback_list_part2]) + + def _format_feedback(self, response_items): + """ + Input: + Dictionary called feedback. Must contain keys seen below. + Output: + Return error message or feedback template + """ + + feedback = self._convert_longform_feedback_to_html(response_items) + + if not response_items['success']: + return self.system.render_template("open_ended_error.html", + {'errors' : feedback}) + + feedback_template = self.system.render_template("open_ended_feedback.html", { + 'grader_type': response_items['grader_type'], + 'score': "{0} / {1}".format(response_items['score'], self.max_score), + 'feedback': feedback, + }) + + return feedback_template + + + def _parse_score_msg(self, score_msg): + """ + Grader reply is a JSON-dump of the following dict + { 'correct': True/False, + 'score': Numeric value (floating point is okay) to assign to answer + 'msg': grader_msg + 'feedback' : feedback from grader + } + + Returns (valid_score_msg, correct, score, msg): + valid_score_msg: Flag indicating valid score_msg format (Boolean) + correct: Correctness of submission (Boolean) + score: Points to be assigned (numeric, can be float) + """ + fail = ScoreMessage(valid=False, correct=False, points=0, msg='') + try: + score_result = json.loads(score_msg) + except (TypeError, ValueError): + log.error("External grader message should be a JSON-serialized dict." + " Received score_msg = {0}".format(score_msg)) + return fail + + if not isinstance(score_result, dict): + log.error("External grader message should be a JSON-serialized dict." + " Received score_result = {0}".format(score_result)) + return fail + + for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: + if tag not in score_result: + log.error("External grader message is missing required tag: {0}" + .format(tag)) + return fail + + feedback = self._format_feedback(score_result) + self.submission_id=score_result['submission_id'] + self.grader_id=score_result['grader_id'] + + # HACK: for now, just assume it's correct if you got more than 2/3. + # Also assumes that score_result['score'] is an integer. + score_ratio = int(score_result['score']) / float(self.max_score) + correct = (score_ratio >= 0.66) + + #Currently ignore msg and only return feedback (which takes the place of msg) + return ScoreMessage(valid=True, correct=correct, + points=score_result['score'], msg=feedback) def handle_ajax(self, dispatch, get): ''' From 54c763d8c46db40bdb1a416559b46821385e0c9d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:19:39 -0500 Subject: [PATCH 073/329] set up template for open ended xmodule --- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/xmodule/open_ended_module.py | 62 +++++++++++++++++++++++-- lms/templates/open_ended.html | 56 ++++++++++++++++++++++ 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 lms/templates/open_ended.html diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index e3eb47acc5..26c97ed907 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -742,7 +742,7 @@ class OpenEndedInput(InputTypeBase): etc. """ - template = "openendedinput.html" + template = tags = ['openendedinput'] # pulled out for testing diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index 0ce2007c21..d087b5310c 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -200,7 +200,7 @@ class OpenEndedModule(): event_info['problem_id'] = self.location.url() event_info['student_id'] = self.system.anonymous_student_id event_info['survey_responses']= get - + survey_responses=event_info['survey_responses'] for tag in ['feedback', 'submission_id', 'grader_id', 'score']: if tag not in survey_responses: @@ -318,7 +318,7 @@ class OpenEndedModule(): return cmap - def update_score(self, score_msg, oldcmap, queuekey): + def _update_score(self, score_msg, oldcmap, queuekey): log.debug(score_msg) score_msg = self._parse_score_msg(score_msg) if not score_msg.valid: @@ -530,7 +530,6 @@ class OpenEndedModule(): 'problem_check': self.check_problem, 'problem_reset': self.reset_problem, 'problem_save': self.save_problem, - 'problem_show': self.get_answer, 'score_update': self.update_score, 'message_post' : self.message_post, } @@ -546,3 +545,60 @@ class OpenEndedModule(): 'progress_status': Progress.to_js_status_str(after), }) return json.dumps(d, cls=ComplexEncoder) + + def get_problem: + return {'html': self.get_problem_html(encapsulate=False)} + + def check_problem: + pass + + def reset_problem: + pass + + def save_problem: + pass + + def update_score: + """ + Delivers grading response (e.g. from asynchronous code checking) to + the capa problem, so its score can be updated + + 'get' must have a field 'response' which is a string that contains the + grader's response + + No ajax return is needed. Return empty dict. + """ + queuekey = get['queuekey'] + score_msg = get['xqueue_body'] + #TODO: Remove need for cmap + self._update_score(score_msg, queuekey) + + return dict() # No AJAX return is needed + + def get_html(self): + """ + Implement special logic: handle queueing state, and default input. + """ + # if no student input yet, then use the default input given by the problem + if not self.value: + self.value = self.xml.text + + # Check if problem has been queued + self.queue_len = 0 + # Flag indicating that the problem has been queued, 'msg' is length of queue + if self.status == 'incomplete': + self.status = 'queued' + self.queue_len = self.msg + self.msg = self.submitted_msg + + context={'rows' : 30, + 'cols' : 80, + 'hidden' : '', + } + + html=self.system.render_template("openendedinput.html", context) + + def _extra_context(self): + """Defined queue_len, add it """ + return {'queue_len': self.queue_len,} + diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html new file mode 100644 index 0000000000..c42ad73faf --- /dev/null +++ b/lms/templates/open_ended.html @@ -0,0 +1,56 @@ +
+ + +
From 56f5434bfdb2100e94e8c82424ccb92fe0552d1f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:22:41 -0500 Subject: [PATCH 074/329] Initial html for open ended xmodule --- common/lib/xmodule/open_ended_module.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index d087b5310c..c86b18e639 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -594,9 +594,14 @@ class OpenEndedModule(): context={'rows' : 30, 'cols' : 80, 'hidden' : '', + 'id' : 'open_ended', + 'msg' : self.msg, + 'status' : self.status, + 'queue_len' : self.queue_len, + 'value' : self.value, } - html=self.system.render_template("openendedinput.html", context) + html=self.system.render_template("open_ended.html", context) def _extra_context(self): """Defined queue_len, add it """ From 1b9deb881bafa3dd7be36a7b94e1771aea8cdbcf Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:29:24 -0500 Subject: [PATCH 075/329] Open ended descriptor --- common/lib/xmodule/open_ended_module.py | 100 +++++++++++++++++++++--- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index c86b18e639..6380dc16d8 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -527,7 +527,6 @@ class OpenEndedModule(): ''' handlers = { 'problem_get': self.get_problem, - 'problem_check': self.check_problem, 'problem_reset': self.reset_problem, 'problem_save': self.save_problem, 'score_update': self.update_score, @@ -547,13 +546,12 @@ class OpenEndedModule(): return json.dumps(d, cls=ComplexEncoder) def get_problem: - return {'html': self.get_problem_html(encapsulate=False)} - - def check_problem: - pass + return self.get_html() def reset_problem: - pass + self.change_state(self.INITIAL) + return {'success': True} + def save_problem: pass @@ -602,8 +600,92 @@ class OpenEndedModule(): } html=self.system.render_template("open_ended.html", context) + return html + + def change_state(self, new_state): + """ + A centralized place for state changes--allows for hooks. If the + current state matches the old state, don't run any hooks. + """ + if self.state == new_state: + return + + self.state = new_state + + if self.state == self.DONE: + self.attempts += 1 + + def get_instance_state(self): + """ + Get the current score and state + """ + + state = { + 'version': self.STATE_VERSION, + 'history': self.history, + 'state': self.state, + 'max_score': self._max_score, + 'attempts': self.attempts, + } + return json.dumps(state) + + +class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): + """ + Module for adding self assessment questions to courses + """ + mako_template = "widgets/html-edit.html" + module_class = OpenEndedModule + filename_extension = "xml" + + stores_state = True + has_score = True + template_dir_name = openended" + + js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} + js_module_name = "HTMLEditingDescriptor" + expected_children = ['rubric', 'prompt', 'oeparam'] + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Pull out the rubric, prompt, and submitmessage into a dictionary. + + Returns: + { + 'rubric': 'some-html', + 'prompt': 'some-html', + 'submitmessage': 'some-html' + 'hintprompt': 'some-html' + } + """ + + for child in self.expected_children: + if len(xml_object.xpath(child)) != 1: + raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) + + def parse(k): + """Assumes that xml_object has child k""" + return stringify_children(xml_object.xpath(k)[0]) + + return {'rubric': parse('rubric'), + 'prompt': parse('prompt'), + 'oeparam': parse('oeparam'), + } + + + def definition_to_xml(self, resource_fs): + '''Return an xml element representing this definition.''' + elt = etree.Element('openended') + + def add_child(k): + child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + child_node = etree.fromstring(child_str) + elt.append(child_node) + + for child in self.expected_children: + add_child(child) + + return elt - def _extra_context(self): - """Defined queue_len, add it """ - return {'queue_len': self.queue_len,} From 9d3f8ed0e52a09ecdc02bdea4edac0679fac9a68 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:37:10 -0500 Subject: [PATCH 076/329] Support open ended in combined open ended module --- common/lib/capa/capa/inputtypes.py | 2 +- .../xmodule/combined_open_ended_module.py | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 26c97ed907..e3eb47acc5 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -742,7 +742,7 @@ class OpenEndedInput(InputTypeBase): etc. """ - template = + template = "openendedinput.html" tags = ['openendedinput'] # pulled out for testing diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f33439673c..e544ebe3ae 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -20,6 +20,7 @@ from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location import self_assessment_module +import open_ended_module log = logging.getLogger("mitx.courseware") @@ -120,6 +121,24 @@ class CombinedOpenEndedModule(XModule): self.state=self.ASSESSING else: self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + elif current_task_type=="openended": + self.current_task_descriptor=open_ended_module.OpenEndedDescriptor(self.system) + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) + if current_task_state is None and self.current_task_number==0: + self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING + elif current_task_state is None and self.current_task_number>0: + last_response, last_score=self.get_last_response(self.current_task_number-1) + current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "history": [{"answer": "' + str(last_response) + '"}]}') + {"state": "done", "version": 1, "max_score": 1, "attempts": 1, "history": [{"answer": "gdgddg", "score": 0, "hint": "dfdfdf"}]} + self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING + else: + self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + return True @@ -158,6 +177,12 @@ class CombinedOpenEndedModule(XModule): task=self_assessment_module.SelfAssessmentModule(self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) last_response=task.latest_answer() last_score = task.latest_score() + elif task_type=="openended": + task_descriptor=open_ended_module.OpenEndedDescriptor(self.system) + task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) + task=open_ended_module.OpenEndedModule(self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) + last_response=task.latest_answer() + last_score = task.latest_score() return last_response, last_score @@ -291,7 +316,7 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' - elt = etree.Element('selfassessment') + elt = etree.Element('combinedopenended') def add_child(k): child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) From dabb5ee758f32ffa45164fe252a7733a397fd37b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:38:56 -0500 Subject: [PATCH 077/329] fix tab issue --- common/lib/xmodule/open_ended_module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/open_ended_module.py index 6380dc16d8..163c518444 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/open_ended_module.py @@ -250,8 +250,7 @@ class OpenEndedModule(): return success, "Successfully submitted your feedback." - def get_score(self, student_answers): - + def get_score(self, student_answers): try: submission = student_answers[self.answer_id] except KeyError: From 0e81f916e1a06710ec70502419b392a38fed034a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:42:25 -0500 Subject: [PATCH 078/329] Move open ended module file --- .../xmodule/combined_open_ended_module.py | 4 ++-- .../{ => xmodule}/open_ended_module.py | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) rename common/lib/xmodule/{ => xmodule}/open_ended_module.py (98%) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index e544ebe3ae..9218de1baf 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -12,6 +12,7 @@ import sys from pkg_resources import resource_string from .capa_module import only_one, ComplexEncoder +from common.lib.xmodule.xmodule import open_ended_module from .editing_module import EditingDescriptor from .html_checker import check_html from progress import Progress @@ -20,7 +21,6 @@ from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location import self_assessment_module -import open_ended_module log = logging.getLogger("mitx.courseware") @@ -122,7 +122,7 @@ class CombinedOpenEndedModule(XModule): else: self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) elif current_task_type=="openended": - self.current_task_descriptor=open_ended_module.OpenEndedDescriptor(self.system) + self.current_task_descriptor= open_ended_module.OpenEndedDescriptor(self.system) self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) if current_task_state is None and self.current_task_number==0: self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) diff --git a/common/lib/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py similarity index 98% rename from common/lib/xmodule/open_ended_module.py rename to common/lib/xmodule/xmodule/open_ended_module.py index 163c518444..f5d6cbf8b5 100644 --- a/common/lib/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -544,18 +544,17 @@ class OpenEndedModule(): }) return json.dumps(d, cls=ComplexEncoder) - def get_problem: + def get_problem(self, get): return self.get_html() - def reset_problem: + def reset_problem(self, get): self.change_state(self.INITIAL) return {'success': True} - - def save_problem: + def save_problem(self, get): pass - def update_score: + def update_score(self, get): """ Delivers grading response (e.g. from asynchronous code checking) to the capa problem, so its score can be updated @@ -602,10 +601,10 @@ class OpenEndedModule(): return html def change_state(self, new_state): - """ - A centralized place for state changes--allows for hooks. If the - current state matches the old state, don't run any hooks. - """ + """ + A centralized place for state changes--allows for hooks. If the + current state matches the old state, don't run any hooks. + """ if self.state == new_state: return @@ -639,7 +638,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): stores_state = True has_score = True - template_dir_name = openended" + template_dir_name = "openended" js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js_module_name = "HTMLEditingDescriptor" From 480978cf3439f3a54ccbdea1f85c38456d14e3ba Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:45:33 -0500 Subject: [PATCH 079/329] Fix invalid import statement --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- common/lib/xmodule/xmodule/open_ended_module.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 9218de1baf..6692dd4947 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -12,7 +12,6 @@ import sys from pkg_resources import resource_string from .capa_module import only_one, ComplexEncoder -from common.lib.xmodule.xmodule import open_ended_module from .editing_module import EditingDescriptor from .html_checker import check_html from progress import Progress @@ -21,6 +20,7 @@ from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location import self_assessment_module +import open_ended_module log = logging.getLogger("mitx.courseware") diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index f5d6cbf8b5..5e8a4b3bec 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -57,8 +57,6 @@ class OpenEndedModule(): DEFAULT_QUEUE = 'open-ended' DEFAULT_MESSAGE_QUEUE = 'open-ended-message' - response_tag = 'openendedresponse' - allowed_inputfields = ['openendedinput'] max_inputfields = 1 STATE_VERSION = 1 From e9d24ad742be1f5ca71c50565a4cb11a41afa27c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:48:51 -0500 Subject: [PATCH 080/329] Fix descriptor --- common/lib/xmodule/xmodule/open_ended_module.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5e8a4b3bec..2ccb89f2cb 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -640,7 +640,6 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js_module_name = "HTMLEditingDescriptor" - expected_children = ['rubric', 'prompt', 'oeparam'] @classmethod def definition_from_xml(cls, xml_object, system): @@ -656,7 +655,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): } """ - for child in self.expected_children: + for child in ['rubric', 'prompt', 'oeparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) @@ -679,7 +678,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in self.expected_children: + for child in ['rubric', 'prompt', 'oeparam']: add_child(child) return elt From 7351317c8ed732327360ec1a58daceeca789c4e8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:51:24 -0500 Subject: [PATCH 081/329] Fix tag naming --- common/lib/xmodule/xmodule/open_ended_module.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 2ccb89f2cb..d6de77d5f4 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -655,7 +655,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): } """ - for child in ['rubric', 'prompt', 'oeparam']: + for child in ['openendedrubric', 'prompt', 'openendedparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) @@ -663,9 +663,9 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return stringify_children(xml_object.xpath(k)[0]) - return {'rubric': parse('rubric'), + return {'rubric': parse('openendedrubric'), 'prompt': parse('prompt'), - 'oeparam': parse('oeparam'), + 'oeparam': parse('openendedparam'), } @@ -678,7 +678,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in ['rubric', 'prompt', 'oeparam']: + for child in ['openendedrubric', 'prompt', 'openendedparam']: add_child(child) return elt From 6e7dae4f670a51c96018a0b41d8e2b25e0b98f4f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 12:53:12 -0500 Subject: [PATCH 082/329] Naming bugfixes --- common/lib/xmodule/xmodule/open_ended_module.py | 4 +--- common/lib/xmodule/xmodule/self_assessment_module.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index d6de77d5f4..4768f53656 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -105,8 +105,6 @@ class OpenEndedModule(): else: instance_state = {} - instance_state = self.convert_state_to_current_format(instance_state) - # History is a list of tuples of (answer, score, hint), where hint may be # None for any element, and score and hint can be None for the last (current) # element. @@ -122,7 +120,7 @@ class OpenEndedModule(): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(instance_state.get('max_score', MAX_SCORE)) - oeparam = definition['openendedparam'] + oeparam = definition['oeparam'] prompt = definition['prompt'] rubric = definition['rubric'] diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 1b10fab9ac..64cf140d38 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -99,7 +99,9 @@ class SelfAssessmentModule(): else: instance_state = {} - # History is a list of tuples of (answer, score, feedback), where hint may be + instance_state = self.convert_state_to_current_format(instance_state) + + # History is a list of tuples of (answer, score, hint), where hint may be # None for any element, and score and hint can be None for the last (current) # element. # Scores are on scale from 0 to max_score From 2891ea4bff3378ea02b01efbf33caad4637be5cc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 13:03:09 -0500 Subject: [PATCH 083/329] Convert away from self.system to just using system --- .../lib/xmodule/xmodule/open_ended_module.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 4768f53656..3da03520ab 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -28,6 +28,7 @@ from .stringify import stringify_children from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location +from capa.util import * log = logging.getLogger("mitx.courseware") @@ -139,9 +140,9 @@ class OpenEndedModule(): if rubric is None: raise ValueError("No rubric found in problem xml.") - self._parse(oeparam, prompt, rubric) + self._parse(oeparam, prompt, rubric, system) - def _parse(self, oeparam, prompt, rubric): + def _parse(self, oeparam, prompt, rubric, system): ''' Parse OpenEndedResponse XML: self.initial_display @@ -151,8 +152,8 @@ class OpenEndedModule(): self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload - prompt_string = self.stringify_children(prompt) - rubric_string = self.stringify_children(rubric) + prompt_string = stringify_children(prompt) + rubric_string = stringify_children(rubric) grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -170,8 +171,8 @@ class OpenEndedModule(): self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') parsed_grader_payload.update({ - 'location' : self.system.location, - 'course_id' : self.system.course_id, + 'location' : system.location, + 'course_id' : system.course_id, 'prompt' : prompt_string, 'rubric' : rubric_string, 'initial_display' : self.initial_display, @@ -186,15 +187,15 @@ class OpenEndedModule(): except ValueError: self.max_score = 1 - def handle_message_post(self,get): + def handle_message_post(self,get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) Returns a boolean success/fail and an error message """ event_info = dict() - event_info['problem_id'] = self.location.url() - event_info['student_id'] = self.system.anonymous_student_id + event_info['problem_id'] = system.location.url() + event_info['student_id'] = system.anonymous_student_id event_info['survey_responses']= get survey_responses=event_info['survey_responses'] @@ -212,15 +213,15 @@ class OpenEndedModule(): log.exception(error_message) return False, "There was an error saving your feedback. Please contact course staff." - qinterface = self.system.xqueue['interface'] + qinterface = system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - anonymous_student_id = self.system.anonymous_student_id - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id = system.anonymous_student_id + queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + anonymous_student_id + self.answer_id) xheader = xqueue_interface.make_xheader( - lms_callback_url=self.system.xqueue['callback_url'], + lms_callback_url=system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.message_queue_name ) @@ -246,7 +247,7 @@ class OpenEndedModule(): return success, "Successfully submitted your feedback." - def get_score(self, student_answers): + def get_score(self, student_answers, system): try: submission = student_answers[self.answer_id] except KeyError: @@ -258,17 +259,17 @@ class OpenEndedModule(): # Prepare xqueue request #------------------------------------------------------------ - qinterface = self.system.xqueue['interface'] + qinterface = system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - anonymous_student_id = self.system.anonymous_student_id + anonymous_student_id = system.anonymous_student_id # Generate header - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + anonymous_student_id + self.answer_id) - xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], + xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) @@ -510,7 +511,7 @@ class OpenEndedModule(): return ScoreMessage(valid=True, correct=correct, points=score_result['score'], msg=feedback) - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, get, system): ''' This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST. @@ -532,7 +533,7 @@ class OpenEndedModule(): return 'Error' before = self.get_progress() - d = handlers[dispatch](get) + d = handlers[dispatch](get, system) after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -540,17 +541,17 @@ class OpenEndedModule(): }) return json.dumps(d, cls=ComplexEncoder) - def get_problem(self, get): - return self.get_html() + def get_problem(self, get, system): + return self.get_html(system) - def reset_problem(self, get): + def reset_problem(self, get, system): self.change_state(self.INITIAL) return {'success': True} - def save_problem(self, get): + def save_problem(self, get, system): pass - def update_score(self, get): + def update_score(self, get, system): """ Delivers grading response (e.g. from asynchronous code checking) to the capa problem, so its score can be updated @@ -563,11 +564,11 @@ class OpenEndedModule(): queuekey = get['queuekey'] score_msg = get['xqueue_body'] #TODO: Remove need for cmap - self._update_score(score_msg, queuekey) + self._update_score(score_msg, queuekey, system) return dict() # No AJAX return is needed - def get_html(self): + def get_html(self, system): """ Implement special logic: handle queueing state, and default input. """ @@ -593,7 +594,7 @@ class OpenEndedModule(): 'value' : self.value, } - html=self.system.render_template("open_ended.html", context) + html=system.render_template("open_ended.html", context) return html def change_state(self, new_state): @@ -659,7 +660,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): def parse(k): """Assumes that xml_object has child k""" - return stringify_children(xml_object.xpath(k)[0]) + return xml_object.xpath(k)[0] return {'rubric': parse('openendedrubric'), 'prompt': parse('prompt'), From a5eec6d60b609ba059b1b871a541d2cb034d1da2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 13:07:12 -0500 Subject: [PATCH 084/329] set location in system --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 6692dd4947..f5014af948 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -75,6 +75,7 @@ class CombinedOpenEndedModule(XModule): # None for any element, and score and hint can be None for the last (current) # element. # Scores are on scale from 0 to max_score + system.set('location', location) self.current_task_number = instance_state.get('current_task_number', 0) self.task_states= instance_state.get('task_states', []) From ef27788cf5cdf784f746a1dbf787f2872bf845cd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 13:14:58 -0500 Subject: [PATCH 085/329] Initial display for open ended problem --- .../lib/xmodule/xmodule/open_ended_module.py | 50 +++++++++++++++---- lms/templates/open_ended.html | 10 ++-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3da03520ab..8edf7e0828 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -573,25 +573,26 @@ class OpenEndedModule(): Implement special logic: handle queueing state, and default input. """ # if no student input yet, then use the default input given by the problem - if not self.value: - self.value = self.xml.text + latest_answer=self.latest_answer() + if latest_answer is None: + value = self.initial_display # Check if problem has been queued self.queue_len = 0 # Flag indicating that the problem has been queued, 'msg' is length of queue - if self.status == 'incomplete': - self.status = 'queued' - self.queue_len = self.msg - self.msg = self.submitted_msg + if self.state == self.ASSESSING: + #self.queue_len = self.msg + #self.msg = self.submitted_msg + pass context={'rows' : 30, 'cols' : 80, 'hidden' : '', 'id' : 'open_ended', - 'msg' : self.msg, - 'status' : self.status, + 'msg' : "This is a message", + 'state' : self.state, 'queue_len' : self.queue_len, - 'value' : self.value, + 'value' : value, } html=system.render_template("open_ended.html", context) @@ -624,6 +625,37 @@ class OpenEndedModule(): } return json.dumps(state) + def latest_answer(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('answer') + + def latest_score(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('score') + + def latest_hint(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('hint') + + def new_history_entry(self, answer): + self.history.append({'answer': answer}) + + def record_latest_score(self, score): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['score'] = score + + def record_latest_hint(self, hint): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['hint'] = hint + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index c42ad73faf..a6ffc684da 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -1,4 +1,4 @@ -
+
- % if status == 'unsubmitted': + % if state == 'initial': Unanswered - % elif status == 'correct': + % elif state == 'done': Correct - % elif status == 'incorrect': + % elif state == 'incorrect': Incorrect - % elif status == 'queued': + % elif state == 'assessing': Submitted for grading % endif From 6a5352620cf1aad9645384fa780fd5e96a8535a6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 13:20:51 -0500 Subject: [PATCH 086/329] Fix display errors --- .../lib/xmodule/xmodule/open_ended_module.py | 83 ++++++++++++------- lms/templates/open_ended.html | 2 +- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 8edf7e0828..bce571ebfc 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -154,6 +154,7 @@ class OpenEndedModule(): # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload prompt_string = stringify_children(prompt) rubric_string = stringify_children(rubric) + self.prompt=prompt_string grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -568,36 +569,6 @@ class OpenEndedModule(): return dict() # No AJAX return is needed - def get_html(self, system): - """ - Implement special logic: handle queueing state, and default input. - """ - # if no student input yet, then use the default input given by the problem - latest_answer=self.latest_answer() - if latest_answer is None: - value = self.initial_display - - # Check if problem has been queued - self.queue_len = 0 - # Flag indicating that the problem has been queued, 'msg' is length of queue - if self.state == self.ASSESSING: - #self.queue_len = self.msg - #self.msg = self.submitted_msg - pass - - context={'rows' : 30, - 'cols' : 80, - 'hidden' : '', - 'id' : 'open_ended', - 'msg' : "This is a message", - 'state' : self.state, - 'queue_len' : self.queue_len, - 'value' : value, - } - - html=system.render_template("open_ended.html", context) - return html - def change_state(self, new_state): """ A centralized place for state changes--allows for hooks. If the @@ -656,6 +627,58 @@ class OpenEndedModule(): history element""" self.history[-1]['hint'] = hint + def _allow_reset(self): + """Can the module be reset?""" + return self.state == self.DONE and self.attempts < self.max_attempts + + def get_html(self, system): + #set context variables and render template + if self.state != self.INITIAL: + latest = self.latest_answer() + previous_answer = latest if latest is not None else '' + else: + previous_answer = '' + + context = { + 'prompt': self.prompt, + 'previous_answer': previous_answer, + 'state': self.state, + 'allow_reset': self._allow_reset(), + 'rows' : 30, + 'cols' : 80, + 'hidden' : '', + 'id' : 'open_ended', + } + + html = system.render_template('open_ended.html', context) + return html + + def max_score(self): + """ + Return max_score + """ + return self._max_score + + def get_score(self): + """ + Returns the last score in the list + """ + score = self.latest_score() + return {'score': score if score is not None else 0, + 'total': self._max_score} + + def get_progress(self): + ''' + For now, just return last score / max_score + ''' + if self._max_score > 0: + try: + return Progress(self.get_score()['score'], self._max_score) + except Exception as err: + log.exception("Got bad progress") + return None + return None + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index a6ffc684da..a3e148d36b 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -1,4 +1,4 @@ -
+
+ >${previous_answer|h}
% if state == 'initial': @@ -21,14 +22,16 @@ % endif
+ + - % if status == 'queued': + % if state == 'assessing': % endif
${msg|n} - % if status in ['correct','incorrect']: + % if state == 'done':
Respond to Feedback From a4ad7800ed7b195019e5d05d9acfb3275b266b40 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 13:51:15 -0500 Subject: [PATCH 088/329] Add proper queue handling --- .../lib/xmodule/xmodule/open_ended_module.py | 48 ++++++++----------- lms/templates/open_ended.html | 2 +- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index b8f3bf8278..9b1b3463c1 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -188,7 +188,7 @@ class OpenEndedModule(): except ValueError: self.max_score = 1 - def handle_message_post(self,get, system): + def message_post(self,get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) Returns a boolean success/fail and an error message @@ -248,14 +248,7 @@ class OpenEndedModule(): return success, "Successfully submitted your feedback." - def get_score(self, student_answers, system): - try: - submission = student_answers[self.answer_id] - except KeyError: - msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}' - .format(self.answer_id, student_answers)) - log.exception(msg) - raise LoncapaProblemError(msg) + def get_score(self, submission, system): # Prepare xqueue request #------------------------------------------------------------ @@ -297,23 +290,7 @@ class OpenEndedModule(): # State associated with the queueing request queuestate = {'key': queuekey, 'time': qtime,} - - cmap = CorrectMap() - if error: - cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: {0}.)' - ' Please try again later.'.format(msg)) - else: - # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that - # the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down - # through inputtypes.textbox and .filesubmission to inform the - # browser that the submission is queued (and it could e.g. poll) - cmap.set(self.answer_id, queuestate=queuestate, - correctness='incomplete', msg=msg) - - return cmap + return True def _update_score(self, score_msg, oldcmap, queuekey): log.debug(score_msg) @@ -550,7 +527,24 @@ class OpenEndedModule(): return {'success': True} def save_problem(self, get, system): - pass + if self.attempts > self.max_attempts: + # If too many attempts, prevent student from saving answer and + # seeing rubric. In normal use, students shouldn't see this because + # they won't see the reset button once they're out of attempts. + return { + 'success': False, + 'error': 'Too many attempts.' + } + + if self.state != self.INITIAL: + return self.out_of_sync_error(get) + + # add new history element with answer and empty score and hint. + self.new_history_entry(get['student_answer']) + self.get_score(get['student_answer'], system) + self.change_state(self.ASSESSING) + + return {'success': True,} def update_score(self, get, system): """ diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 3456b61068..876d9a5ca1 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -23,7 +23,7 @@
- + % if state == 'assessing': From fc02e7e2692c4aa37ebeaf38003e31ee0f83dd1c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:00:15 -0500 Subject: [PATCH 089/329] Working on bugfixing --- common/lib/xmodule/xmodule/open_ended_module.py | 17 ++++++----------- lms/templates/open_ended.html | 8 ++++++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 9b1b3463c1..3dc4a787d5 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -30,6 +30,8 @@ from .xml_module import XmlDescriptor from xmodule.modulestore import Location from capa.util import * +from datetime import datetime + log = logging.getLogger("mitx.courseware") # Set the default number of max attempts. Should be 1 for production @@ -261,7 +263,7 @@ class OpenEndedModule(): # Generate header queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + anonymous_student_id + - self.answer_id) + 1) xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'], lms_key=queuekey, @@ -499,10 +501,11 @@ class OpenEndedModule(): 'progress' : 'none'/'in_progress'/'done', } ''' + log.debug(get) handlers = { 'problem_get': self.get_problem, 'problem_reset': self.reset_problem, - 'problem_save': self.save_problem, + 'save_answer': self.save_answer, 'score_update': self.update_score, 'message_post' : self.message_post, } @@ -526,7 +529,7 @@ class OpenEndedModule(): self.change_state(self.INITIAL) return {'success': True} - def save_problem(self, get, system): + def save_answer(self, get, system): if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and # seeing rubric. In normal use, students shouldn't see this because @@ -654,14 +657,6 @@ class OpenEndedModule(): """ return self._max_score - def get_score(self): - """ - Returns the last score in the list - """ - score = self.latest_score() - return {'score': score if score is not None else 0, - 'total': self._max_score} - def get_progress(self): ''' For now, just return last score / max_score diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 876d9a5ca1..d682de5291 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -1,12 +1,16 @@
+ +
+
${prompt|n} - -
+
% if state == 'initial': Unanswered % elif state == 'done': From 825821ae885b178e1a70a481385356358e786d8f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:05:34 -0500 Subject: [PATCH 090/329] Fix key generation --- common/lib/xmodule/xmodule/open_ended_module.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3dc4a787d5..fbd0662a63 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -263,14 +263,12 @@ class OpenEndedModule(): # Generate header queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + anonymous_student_id + - 1) + str(len(self.history))) xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) - self.context.update({'submission': submission}) - contents = self.payload.copy() # Metadata related to the student submission revealed to the external grader @@ -657,13 +655,21 @@ class OpenEndedModule(): """ return self._max_score + def get_score_value(self): + """ + Returns the last score in the list + """ + score = self.latest_score() + return {'score': score if score is not None else 0, + 'total': self._max_score} + def get_progress(self): ''' For now, just return last score / max_score ''' if self._max_score > 0: try: - return Progress(self.get_score()['score'], self._max_score) + return Progress(self.get_score_value()['score'], self._max_score) except Exception as err: log.exception("Got bad progress") return None From c3a91f59083191462ee0b74b22fd553c811ddc65 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:08:51 -0500 Subject: [PATCH 091/329] Replace score message with dictionary --- common/lib/xmodule/xmodule/open_ended_module.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index fbd0662a63..dcc5e2a3da 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -457,7 +457,7 @@ class OpenEndedModule(): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) """ - fail = ScoreMessage(valid=False, correct=False, points=0, msg='') + fail = {'valid' : False, 'correct' : False, 'points' : 0, 'msg' : ''} try: score_result = json.loads(score_msg) except (TypeError, ValueError): @@ -486,8 +486,7 @@ class OpenEndedModule(): correct = (score_ratio >= 0.66) #Currently ignore msg and only return feedback (which takes the place of msg) - return ScoreMessage(valid=True, correct=correct, - points=score_result['score'], msg=feedback) + return {'valid' : True, 'correct' : correct, 'points' : score_result['score'], 'msg' : feedback} def handle_ajax(self, dispatch, get, system): ''' From d02b9d9ce67484226a23786d4ce0f92253e581c1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:15:39 -0500 Subject: [PATCH 092/329] Remove correct map and place state changes in its place --- .../lib/xmodule/xmodule/open_ended_module.py | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index dcc5e2a3da..4b7dd8e95b 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -295,32 +295,17 @@ class OpenEndedModule(): def _update_score(self, score_msg, oldcmap, queuekey): log.debug(score_msg) score_msg = self._parse_score_msg(score_msg) - if not score_msg.valid: - oldcmap.set(self.answer_id, - msg = 'Invalid grader reply. Please contact the course staff.') + if not score_msg['valid']: + score_msg['msg'] = 'Invalid grader reply. Please contact the course staff.' return oldcmap correctness = 'correct' if score_msg.correct else 'incorrect' - # TODO: Find out how this is used elsewhere, if any - self.context['correct'] = correctness + self._record_latest_score(score_msg['points']) + self._record_latest_feedback(score_msg['msg']) + self.state=self.POST_ASSESSMENT - # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey - # does not match, we keep waiting for the score_msg whose key actually matches - if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points - points = score_msg.points - if points < 0: - points = 0 - - # Queuestate is consumed, so reset it to None - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, - msg = score_msg.msg.replace(' ', ' '), queuestate=None) - else: - log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format( - queuekey, self.answer_id)) - - return oldcmap + return True def get_answers(self): @@ -602,11 +587,11 @@ class OpenEndedModule(): return None return self.history[-1].get('score') - def latest_hint(self): + def latest_feedback(self): """None if not available""" if not self.history: return None - return self.history[-1].get('hint') + return self.history[-1].get('feedback') def new_history_entry(self, answer): self.history.append({'answer': answer}) @@ -616,10 +601,10 @@ class OpenEndedModule(): history element""" self.history[-1]['score'] = score - def record_latest_hint(self, hint): + def record_latest_feedback(self, feedback): """Assumes that state is right, so we're adding a score to the latest history element""" - self.history[-1]['hint'] = hint + self.history[-1]['feedback'] = feedback def _allow_reset(self): """Can the module be reset?""" @@ -699,8 +684,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): { 'rubric': 'some-html', 'prompt': 'some-html', - 'submitmessage': 'some-html' - 'hintprompt': 'some-html' + 'oeparam': 'some-html' } """ From a6c273bd05160e415cc1d939194713f2ce7909c5 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:18:07 -0500 Subject: [PATCH 093/329] More informative dictionary keys --- .../lib/xmodule/xmodule/open_ended_module.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 4b7dd8e95b..38180ebb5b 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -296,13 +296,10 @@ class OpenEndedModule(): log.debug(score_msg) score_msg = self._parse_score_msg(score_msg) if not score_msg['valid']: - score_msg['msg'] = 'Invalid grader reply. Please contact the course staff.' - return oldcmap + score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' - correctness = 'correct' if score_msg.correct else 'incorrect' - - self._record_latest_score(score_msg['points']) - self._record_latest_feedback(score_msg['msg']) + self._record_latest_score(score_msg['score']) + self._record_latest_feedback(score_msg['feedback']) self.state=self.POST_ASSESSMENT return True @@ -465,13 +462,12 @@ class OpenEndedModule(): self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] - # HACK: for now, just assume it's correct if you got more than 2/3. - # Also assumes that score_result['score'] is an integer. - score_ratio = int(score_result['score']) / float(self.max_score) - correct = (score_ratio >= 0.66) + return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} - #Currently ignore msg and only return feedback (which takes the place of msg) - return {'valid' : True, 'correct' : correct, 'points' : score_result['score'], 'msg' : feedback} + def is_submission_correct(self, score): + score_ratio = int(score) / float(self.max_score) + correct = (score_ratio >= 0.66) + return correct def handle_ajax(self, dispatch, get, system): ''' From 9cf4dc52f18779d6f644e380efa6476d6dbee34e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:20:57 -0500 Subject: [PATCH 094/329] Fix passing of system object --- common/lib/xmodule/xmodule/open_ended_module.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 38180ebb5b..defbd17ad3 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -292,9 +292,9 @@ class OpenEndedModule(): 'time': qtime,} return True - def _update_score(self, score_msg, oldcmap, queuekey): + def _update_score(self, score_msg, queuekey, system): log.debug(score_msg) - score_msg = self._parse_score_msg(score_msg) + score_msg = self._parse_score_msg(score_msg, system) if not score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' @@ -402,7 +402,7 @@ class OpenEndedModule(): return u"\n".join([feedback_list_part1,feedback_list_part2]) - def _format_feedback(self, response_items): + def _format_feedback(self, response_items, system): """ Input: Dictionary called feedback. Must contain keys seen below. @@ -413,10 +413,10 @@ class OpenEndedModule(): feedback = self._convert_longform_feedback_to_html(response_items) if not response_items['success']: - return self.system.render_template("open_ended_error.html", + return system.render_template("open_ended_error.html", {'errors' : feedback}) - feedback_template = self.system.render_template("open_ended_feedback.html", { + feedback_template = system.render_template("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score), 'feedback': feedback, @@ -425,7 +425,7 @@ class OpenEndedModule(): return feedback_template - def _parse_score_msg(self, score_msg): + def _parse_score_msg(self, score_msg, system): """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, @@ -458,7 +458,7 @@ class OpenEndedModule(): .format(tag)) return fail - feedback = self._format_feedback(score_result) + feedback = self._format_feedback(score_result, system) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From 507457dc59aab7d0d7af6d377332ee28832e81c8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:22:14 -0500 Subject: [PATCH 095/329] Fix function naming --- common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index defbd17ad3..febc74895a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -298,8 +298,8 @@ class OpenEndedModule(): if not score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' - self._record_latest_score(score_msg['score']) - self._record_latest_feedback(score_msg['feedback']) + self.record_latest_score(score_msg['score']) + self.record_latest_feedback(score_msg['feedback']) self.state=self.POST_ASSESSMENT return True From 3a6c660b522bca09926ce1f08d0c7b6060b030e5 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:24:30 -0500 Subject: [PATCH 096/329] Now display grader message --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index febc74895a..919bc9a0dc 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -623,7 +623,7 @@ class OpenEndedModule(): 'cols' : 80, 'hidden' : '', 'id' : 'open_ended', - 'msg' : "", + 'msg' : self.latest_feedback(), } html = system.render_template('open_ended.html', context) From 254eae034be05c881645a4c9c7367b74c7c8824f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:47:00 -0500 Subject: [PATCH 097/329] Add in support for passing problem type to javascript --- .../js/src/combinedopenended/display.coffee | 39 +++++++++++++++++-- .../lib/xmodule/xmodule/open_ended_module.py | 3 +- .../xmodule/xmodule/self_assessment_module.py | 3 +- lms/templates/open_ended.html | 7 +--- lms/templates/self_assessment_hint.html | 2 +- lms/templates/self_assessment_prompt.html | 2 +- 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 08c7aafa4d..b0eed2e896 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -25,6 +25,7 @@ class @CombinedOpenEnded @message_wrapper = @$('.message-wrapper') @submit_button = @$('.submit-button') @child_state = @el.data('state') + @child_type = @el.data('child-type') @open_ended_child= @$('.open-ended-child') @@ -54,7 +55,7 @@ class @CombinedOpenEnded @submit_button.click @save_assessment else if @child_state == 'post_assessment' @answer_area.attr("disabled", true) - @submit_button.prop('value', 'Submit hint') + @submit_button.prop('value', 'Submit post-assessment') @submit_button.click @save_hint else if @child_state == 'done' @answer_area.attr("disabled", true) @@ -74,7 +75,8 @@ class @CombinedOpenEnded @assessment = @$('select.assessment') find_hint_elements: -> - @hint_area = @$('textarea.hint') + @hint_area = @$('textarea.post_assessment') + @hint_box = @$('') save_answer: (event) => event.preventDefault() @@ -118,7 +120,7 @@ class @CombinedOpenEnded if @child_state == 'post_assessment' data = {'hint' : @hint_area.val()} - $.postWithPrefix "#{@ajax_url}/save_hint", data, (response) => + $.postWithPrefix "#{@ajax_url}/save_post_assessment", data, (response) => if response.success @message_wrapper.html(response.message_html) @child_state = 'done' @@ -164,4 +166,33 @@ class @CombinedOpenEnded else @errors_area.html(response.error) else - @errors_area.html('Problem state got out of sync. Try reloading the page.') \ No newline at end of file + @errors_area.html('Problem state got out of sync. Try reloading the page.') + + message_post: => + Logger.log 'message_post', @answers + + fd = new FormData() + feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value + submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML + grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML + score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() + fd.append('feedback', feedback) + fd.append('submission_id', submission_id) + fd.append('grader_id', grader_id) + if(!score) + @gentle_alert "You need to pick a rating before you can submit." + return + else + fd.append('score', score) + + + settings = + type: "POST" + data: fd + processData: false + contentType: false + success: (response) => + @gentle_alert response.message + @$('section.evaluation').slideToggle() + + $.ajaxWithPrefix("#{@url}/message_post", settings) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 919bc9a0dc..24b00a0126 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -485,7 +485,7 @@ class OpenEndedModule(): 'problem_reset': self.reset_problem, 'save_answer': self.save_answer, 'score_update': self.update_score, - 'message_post' : self.message_post, + 'save_post_assessment' : self.message_post, } if dispatch not in handlers: @@ -624,6 +624,7 @@ class OpenEndedModule(): 'hidden' : '', 'id' : 'open_ended', 'msg' : self.latest_feedback(), + 'child_type' : 'openended', } html = system.render_template('open_ended.html', context) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 64cf140d38..ee35ff801c 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -235,6 +235,7 @@ class SelfAssessmentModule(): 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), + 'child_type' : 'selfassessment', } html = system.render_template('self_assessment_prompt.html', context) @@ -281,7 +282,7 @@ class SelfAssessmentModule(): handlers = { 'save_answer': self.save_answer, 'save_assessment': self.save_assessment, - 'save_hint': self.save_hint, + 'save_post_assessment': self.save_hint, 'reset': self.reset, } diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index d682de5291..a40f0e189e 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -1,4 +1,4 @@ -
+
% endif diff --git a/lms/templates/self_assessment_hint.html b/lms/templates/self_assessment_hint.html index 64c45b809e..1adfc69e39 100644 --- a/lms/templates/self_assessment_hint.html +++ b/lms/templates/self_assessment_hint.html @@ -2,6 +2,6 @@
${hint_prompt}
-
diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 91223fdaa1..479e42a126 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -1,5 +1,5 @@
+ data-id="${id}" data-state="${state}" data-allow_reset="${allow_reset}" data-child-type="${child_type}">
${prompt} From ca234dc900136e50981e2e150081f765c061dc86 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 14:59:11 -0500 Subject: [PATCH 098/329] Fixing message post --- .../js/src/combinedopenended/display.coffee | 14 +++++++++++--- common/lib/xmodule/xmodule/open_ended_module.py | 6 +++--- lms/templates/open_ended.html | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b0eed2e896..30b2e6b965 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -56,7 +56,10 @@ class @CombinedOpenEnded else if @child_state == 'post_assessment' @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') - @submit_button.click @save_hint + if @child_type=="selfassessment" + @submit_button.click @save_hint + else + @submit_button.click @message_post else if @child_state == 'done' @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @@ -76,7 +79,6 @@ class @CombinedOpenEnded find_hint_elements: -> @hint_area = @$('textarea.post_assessment') - @hint_box = @$('') save_answer: (event) => event.preventDefault() @@ -195,4 +197,10 @@ class @CombinedOpenEnded @gentle_alert response.message @$('section.evaluation').slideToggle() - $.ajaxWithPrefix("#{@url}/message_post", settings) \ No newline at end of file + $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) + + gentle_alert: (msg) => + if @el.find('.open-ended-alert').length + @el.find('.open-ended-alert').remove() + alert_elem = "
" + msg + "
" + @el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 24b00a0126..a7d34b5636 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -204,7 +204,7 @@ class OpenEndedModule(): survey_responses=event_info['survey_responses'] for tag in ['feedback', 'submission_id', 'grader_id', 'score']: if tag not in survey_responses: - return False, "Could not find needed tag {0}".format(tag) + return {'success' : False, 'msg' : "Could not find needed tag {0}".format(tag)} try: submission_id=int(survey_responses['submission_id']) grader_id = int(survey_responses['grader_id']) @@ -214,7 +214,7 @@ class OpenEndedModule(): error_message=("Could not parse submission id, grader id, " "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) log.exception(error_message) - return False, "There was an error saving your feedback. Please contact course staff." + return {'success' : False, 'msg' : "There was an error saving your feedback. Please contact course staff."} qinterface = system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -248,7 +248,7 @@ class OpenEndedModule(): if error: success=False - return success, "Successfully submitted your feedback." + return {'success' : success, 'msg' : "Successfully submitted your feedback."} def get_score(self, submission, system): diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index a40f0e189e..fae2b45041 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -57,4 +57,5 @@
% endif
+
From 45036cb6544b829aad147ec642c6dc74c3269df8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:01:48 -0500 Subject: [PATCH 099/329] Message post queuekey change --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index a7d34b5636..5bee2e67a6 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -221,7 +221,7 @@ class OpenEndedModule(): anonymous_student_id = system.anonymous_student_id queuekey = xqueue_interface.make_hashkey(str(system.seed) + qtime + anonymous_student_id + - self.answer_id) + str(len(self.history))) xheader = xqueue_interface.make_xheader( lms_callback_url=system.xqueue['callback_url'], From d2ef85cce54058de7fbb70807fc1427231e4c35b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:03:07 -0500 Subject: [PATCH 100/329] Finalize submission after message post --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++++ common/lib/xmodule/xmodule/open_ended_module.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 30b2e6b965..d02db0b932 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -196,6 +196,10 @@ class @CombinedOpenEnded success: (response) => @gentle_alert response.message @$('section.evaluation').slideToggle() + @message_wrapper.html(response.message_html) + @child_state = 'done' + @allow_reset = response.allow_reset + @rebind() $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5bee2e67a6..96cc87da3a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -248,6 +248,8 @@ class OpenEndedModule(): if error: success=False + self.state=self.DONE + return {'success' : success, 'msg' : "Successfully submitted your feedback."} def get_score(self, submission, system): From 79fdb79ed4fb0189023f26d1c46758c7fd789d5c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:17:06 -0500 Subject: [PATCH 101/329] Fixing states and html display --- .../xmodule/xmodule/combined_open_ended_module.py | 2 +- common/lib/xmodule/xmodule/open_ended_module.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f5014af948..cb05074b34 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -131,7 +131,7 @@ class CombinedOpenEndedModule(XModule): self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) - current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + current_task_state = ('{"state": "initial", "version": 1, "max_score": ' + str(self._max_score) + ', ' + '"attempts": 0, "history": [{"answer": "' + str(last_response) + '"}]}') {"state": "done", "version": 1, "max_score": 1, "attempts": 1, "history": [{"answer": "gdgddg", "score": 0, "hint": "dfdfdf"}]} self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 96cc87da3a..2cc436a808 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -576,20 +576,20 @@ class OpenEndedModule(): def latest_answer(self): """None if not available""" if not self.history: - return None - return self.history[-1].get('answer') + return "" + return self.history[-1].get('answer', "") def latest_score(self): """None if not available""" if not self.history: - return None - return self.history[-1].get('score') + return "" + return self.history[-1].get('score', "") def latest_feedback(self): """None if not available""" if not self.history: - return None - return self.history[-1].get('feedback') + return "" + return self.history[-1].get('feedback', "") def new_history_entry(self, answer): self.history.append({'answer': answer}) From 85776fbe3064bfd7cd7a328653a702c9fe608905 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:23:41 -0500 Subject: [PATCH 102/329] Fix reset in open ended --- .../lib/xmodule/xmodule/open_ended_module.py | 21 ++++++++++++++++++- .../xmodule/xmodule/self_assessment_module.py | 1 - 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 2cc436a808..7127ef2388 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -484,7 +484,6 @@ class OpenEndedModule(): log.debug(get) handlers = { 'problem_get': self.get_problem, - 'problem_reset': self.reset_problem, 'save_answer': self.save_answer, 'score_update': self.update_score, 'save_post_assessment' : self.message_post, @@ -509,6 +508,16 @@ class OpenEndedModule(): self.change_state(self.INITIAL) return {'success': True} + def out_of_sync_error(self, get, msg=''): + """ + return dict out-of-sync error message, and also log. + """ + log.warning("Assessment module state out sync. state: %r, get: %r. %s", + self.state, get, msg) + return {'success': False, + 'error': 'The problem state got out-of-sync'} + + def save_answer(self, get, system): if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and @@ -658,6 +667,16 @@ class OpenEndedModule(): return None return None + def reset(self, system): + """ + If resetting is allowed, reset the state. + + Returns {'success': bool, 'error': msg} + (error only present if not success) + """ + self.change_state(self.INITIAL) + return {'success': True} + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index ee35ff801c..925083c97a 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -283,7 +283,6 @@ class SelfAssessmentModule(): 'save_answer': self.save_answer, 'save_assessment': self.save_assessment, 'save_post_assessment': self.save_hint, - 'reset': self.reset, } if dispatch not in handlers: From f3ca5f456fc94bb2a9305374535722cbc84fad7a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:32:56 -0500 Subject: [PATCH 103/329] Save answer based on history --- common/lib/xmodule/xmodule/open_ended_module.py | 7 +++++-- lms/templates/open_ended.html | 6 +----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 7127ef2388..ad338203f1 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -532,7 +532,11 @@ class OpenEndedModule(): return self.out_of_sync_error(get) # add new history element with answer and empty score and hint. - self.new_history_entry(get['student_answer']) + if(len(self.history)>0): + if(len(self.history[-1].keys())>1): + self.new_history_entry(get['student_answer']) + else: + get['student_answer']=self.latest_answer() self.get_score(get['student_answer'], system) self.change_state(self.ASSESSING) @@ -632,7 +636,6 @@ class OpenEndedModule(): 'allow_reset': self._allow_reset(), 'rows' : 30, 'cols' : 80, - 'hidden' : '', 'id' : 'open_ended', 'msg' : self.latest_feedback(), 'child_type' : 'openended', diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index fae2b45041..fbf8d74d16 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -4,11 +4,7 @@
${prompt|n} - +
% if state == 'initial': From f131b8673d7c6f93dd404d8d43e721f042c0bdea Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 15:54:53 -0500 Subject: [PATCH 104/329] Handle multiple chained submissions --- .../xmodule/xmodule/combined_open_ended_module.py | 8 +++----- common/lib/xmodule/xmodule/open_ended_module.py | 15 ++++++++++----- .../lib/xmodule/xmodule/self_assessment_module.py | 3 +++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index cb05074b34..d08eef7253 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -115,8 +115,7 @@ class CombinedOpenEndedModule(XModule): elif current_task_state is None and self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "history": [{"answer": "' + str(last_response) + '"}]}') - {"state": "done", "version": 1, "max_score": 1, "attempts": 1, "history": [{"answer": "gdgddg", "score": 0, "hint": "dfdfdf"}]} + '"attempts": 0, "created": True, "history": [{"answer": "' + str(last_response) + '"}]}') self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING @@ -131,9 +130,8 @@ class CombinedOpenEndedModule(XModule): self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) - current_task_state = ('{"state": "initial", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "history": [{"answer": "' + str(last_response) + '"}]}') - {"state": "done", "version": 1, "max_score": 1, "attempts": 1, "history": [{"answer": "gdgddg", "score": 0, "hint": "dfdfdf"}]} + current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "created": True, "history": [{"answer": "' + str(last_response) + '"}]}') self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ad338203f1..02ce31cea0 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -116,6 +116,14 @@ class OpenEndedModule(): self.state = instance_state.get('state', 'initial') + self.created = instance_state.get('created', False) + + if self.created and self.state == self.ASSESSING: + self.created=False + self.get_score(self.latest_answer(), system) + + self.created=False + self.attempts = instance_state.get('attempts', 0) self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) @@ -532,11 +540,7 @@ class OpenEndedModule(): return self.out_of_sync_error(get) # add new history element with answer and empty score and hint. - if(len(self.history)>0): - if(len(self.history[-1].keys())>1): - self.new_history_entry(get['student_answer']) - else: - get['student_answer']=self.latest_answer() + self.new_history_entry(get['student_answer']) self.get_score(get['student_answer'], system) self.change_state(self.ASSESSING) @@ -583,6 +587,7 @@ class OpenEndedModule(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, + 'created' : self.created, } return json.dumps(state) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 925083c97a..56178acee4 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -107,6 +107,8 @@ class SelfAssessmentModule(): # Scores are on scale from 0 to max_score self.history = instance_state.get('history', []) + self.created = instance_state.get('created', False) + self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) @@ -489,6 +491,7 @@ class SelfAssessmentModule(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, + 'created' : self.created, } return json.dumps(state) From d3ccc64846ebd33ffc43992d2d7f507f5a8b9dfb Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 16:06:41 -0500 Subject: [PATCH 105/329] Remove some unneeded log statements --- .../xmodule/combined_open_ended_module.py | 6 +++--- .../lib/xmodule/xmodule/open_ended_module.py | 18 ++++++++---------- .../xmodule/xmodule/self_assessment_module.py | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index d08eef7253..a5fa059a37 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -101,7 +101,6 @@ class CombinedOpenEndedModule(XModule): if self.state in [self.ASSESSING, self.DONE]: current_task_state=self.task_states[len(self.task_states)-1] - log.debug(self.task_states) self.current_task_xml=self.task_xml[self.current_task_number] current_task_type=self.get_tag_name(self.current_task_xml) @@ -115,7 +114,7 @@ class CombinedOpenEndedModule(XModule): elif current_task_state is None and self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": True, "history": [{"answer": "' + str(last_response) + '"}]}') + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING @@ -131,7 +130,8 @@ class CombinedOpenEndedModule(XModule): elif current_task_state is None and self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": True, "history": [{"answer": "' + str(last_response) + '"}]}') + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') + log.debug(current_task_state) self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 02ce31cea0..d3d04fe2ec 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -104,6 +104,7 @@ class OpenEndedModule(): # Load instance state if instance_state is not None: + log.debug(instance_state) instance_state = json.loads(instance_state) else: instance_state = {} @@ -116,13 +117,7 @@ class OpenEndedModule(): self.state = instance_state.get('state', 'initial') - self.created = instance_state.get('created', False) - - if self.created and self.state == self.ASSESSING: - self.created=False - self.get_score(self.latest_answer(), system) - - self.created=False + self.created = instance_state.get('created', "False") self.attempts = instance_state.get('attempts', 0) self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) @@ -152,6 +147,11 @@ class OpenEndedModule(): self._parse(oeparam, prompt, rubric, system) + if self.created=="True" and self.state == self.ASSESSING: + self.created="False" + self.get_score(self.latest_answer(), system) + self.created="False" + def _parse(self, oeparam, prompt, rubric, system): ''' Parse OpenEndedResponse XML: @@ -303,7 +303,6 @@ class OpenEndedModule(): return True def _update_score(self, score_msg, queuekey, system): - log.debug(score_msg) score_msg = self._parse_score_msg(score_msg, system) if not score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' @@ -489,7 +488,6 @@ class OpenEndedModule(): 'progress' : 'none'/'in_progress'/'done', } ''' - log.debug(get) handlers = { 'problem_get': self.get_problem, 'save_answer': self.save_answer, @@ -587,7 +585,7 @@ class OpenEndedModule(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, - 'created' : self.created, + 'created' : "False", } return json.dumps(state) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 56178acee4..64edb2bb8a 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -107,7 +107,7 @@ class SelfAssessmentModule(): # Scores are on scale from 0 to max_score self.history = instance_state.get('history', []) - self.created = instance_state.get('created', False) + self.created = instance_state.get('created', "False") self.state = instance_state.get('state', 'initial') @@ -491,7 +491,7 @@ class SelfAssessmentModule(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, - 'created' : self.created, + 'created' : "False", } return json.dumps(state) From a39ce505d8486167234fb798837c3cfff11c5720 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 16:34:55 -0500 Subject: [PATCH 106/329] Fix loading for responses after attempt 1 --- .../xmodule/combined_open_ended_module.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a5fa059a37..a94eb1f170 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -101,7 +101,6 @@ class CombinedOpenEndedModule(XModule): if self.state in [self.ASSESSING, self.DONE]: current_task_state=self.task_states[len(self.task_states)-1] - self.current_task_xml=self.task_xml[self.current_task_number] current_task_type=self.get_tag_name(self.current_task_xml) if current_task_type=="selfassessment": @@ -119,7 +118,14 @@ class CombinedOpenEndedModule(XModule): self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING else: + if self.current_task_number>0: + last_response, last_score=self.get_last_response(self.current_task_number-1) + loaded_task_state=json.loads(current_task_state) + loaded_task_state['state']=self.ASSESSING + loaded_task_state['created'] = "True" + loaded_task_state['history'].append({'answer' : last_response}) self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + elif current_task_type=="openended": self.current_task_descriptor= open_ended_module.OpenEndedDescriptor(self.system) self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) @@ -131,14 +137,19 @@ class CombinedOpenEndedModule(XModule): last_response, last_score=self.get_last_response(self.current_task_number-1) current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - log.debug(current_task_state) self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING else: + if self.current_task_number>0: + last_response, last_score=self.get_last_response(self.current_task_number-1) + loaded_task_state=json.loads(current_task_state) + loaded_task_state['state']=self.ASSESSING + loaded_task_state['created'] = "True" + loaded_task_state['history'].append({'answer' : last_response}) self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) - + log.debug(self.current_task.get_instance_state()) return True def get_html(self): @@ -231,7 +242,7 @@ class CombinedOpenEndedModule(XModule): return json.dumps(d,cls=ComplexEncoder) def next_problem(self, get): - self.setup_next_task() + self.update_task_states() return {'success' : True} def reset(self, get): From 54db9fd03ce78964d05a7185add5352d615d4063 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 16:47:52 -0500 Subject: [PATCH 107/329] Fix state tracking? --- .../xmodule/combined_open_ended_module.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a94eb1f170..006014700e 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -98,8 +98,8 @@ class CombinedOpenEndedModule(XModule): def setup_next_task(self): current_task_state=None - if self.state in [self.ASSESSING, self.DONE]: - current_task_state=self.task_states[len(self.task_states)-1] + if len(self.task_states)>self.current_task_number: + current_task_state=self.task_states[self.current_task_number] self.current_task_xml=self.task_xml[self.current_task_number] current_task_type=self.get_tag_name(self.current_task_xml) @@ -121,9 +121,11 @@ class CombinedOpenEndedModule(XModule): if self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) loaded_task_state=json.loads(current_task_state) - loaded_task_state['state']=self.ASSESSING - loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) + if loaded_task_state['state']== self.INITIAL: + loaded_task_state['state']=self.ASSESSING + loaded_task_state['created'] = "True" + loaded_task_state['history'].append({'answer' : last_response}) + current_task_state=json.dumps(loaded_task_state) self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) elif current_task_type=="openended": @@ -144,9 +146,11 @@ class CombinedOpenEndedModule(XModule): if self.current_task_number>0: last_response, last_score=self.get_last_response(self.current_task_number-1) loaded_task_state=json.loads(current_task_state) - loaded_task_state['state']=self.ASSESSING - loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) + if loaded_task_state['state']== self.INITIAL: + loaded_task_state['state']=self.ASSESSING + loaded_task_state['created'] = "True" + loaded_task_state['history'].append({'answer' : last_response}) + current_task_state=json.dumps(loaded_task_state) self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) log.debug(self.current_task.get_instance_state()) From 332ad89b6ab34b56c55ba3f43088863a13cc0d7b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 17:01:41 -0500 Subject: [PATCH 108/329] Streamline modules --- .../xmodule/combined_open_ended_module.py | 92 +++++++++---------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 006014700e..db69c2dae3 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -95,6 +95,30 @@ class CombinedOpenEndedModule(XModule): tag=etree.fromstring(xml).tag return tag + def overwrite_state(self, current_task_state): + last_response, last_score=self.get_last_response(self.current_task_number-1) + loaded_task_state=json.loads(current_task_state) + if loaded_task_state['state']== self.INITIAL: + loaded_task_state['state']=self.ASSESSING + loaded_task_state['created'] = "True" + loaded_task_state['history'].append({'answer' : last_response}) + current_task_state=json.dumps(loaded_task_state) + return current_task_state + + def child_modules(self): + child_modules={ + 'openended' : open_ended_module.OpenEndedModule, + 'selfassessment' : self_assessment_module.SelfAssessmentModule, + } + child_descriptors={ + 'openended' : open_ended_module.OpenEndedDescriptor, + 'selfassessment' : self_assessment_module.SelfAssessmentDescriptor, + } + children={ + 'modules' : child_modules, + 'descriptors' : child_descriptors, + } + return children def setup_next_task(self): current_task_state=None @@ -103,55 +127,26 @@ class CombinedOpenEndedModule(XModule): self.current_task_xml=self.task_xml[self.current_task_number] current_task_type=self.get_tag_name(self.current_task_xml) - if current_task_type=="selfassessment": - self.current_task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) - if current_task_state is None and self.current_task_number==0: - self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) - self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - elif current_task_state is None and self.current_task_number>0: - last_response, last_score=self.get_last_response(self.current_task_number-1) - current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) - self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - else: - if self.current_task_number>0: - last_response, last_score=self.get_last_response(self.current_task_number-1) - loaded_task_state=json.loads(current_task_state) - if loaded_task_state['state']== self.INITIAL: - loaded_task_state['state']=self.ASSESSING - loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) - current_task_state=json.dumps(loaded_task_state) - self.current_task=self_assessment_module.SelfAssessmentModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) - elif current_task_type=="openended": - self.current_task_descriptor= open_ended_module.OpenEndedDescriptor(self.system) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) - if current_task_state is None and self.current_task_number==0: - self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) - self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - elif current_task_state is None and self.current_task_number>0: - last_response, last_score=self.get_last_response(self.current_task_number-1) - current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) - self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - else: - if self.current_task_number>0: - last_response, last_score=self.get_last_response(self.current_task_number-1) - loaded_task_state=json.loads(current_task_state) - if loaded_task_state['state']== self.INITIAL: - loaded_task_state['state']=self.ASSESSING - loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) - current_task_state=json.dumps(loaded_task_state) - self.current_task=open_ended_module.OpenEndedModule(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + children=self.child_modules() + + self.current_task_descriptor=children['descriptors'][current_task_type](self.system) + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) + if current_task_state is None and self.current_task_number==0: + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING + elif current_task_state is None and self.current_task_number>0: + last_response, last_score=self.get_last_response(self.current_task_number-1) + current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + self.task_states.append(self.current_task.get_instance_state()) + self.state=self.ASSESSING + else: + if self.current_task_number>0: + current_task_state=self.overwrite_state(current_task_state) + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) log.debug(self.current_task.get_instance_state()) return True @@ -270,6 +265,7 @@ class CombinedOpenEndedModule(XModule): self.setup_next_task() self.current_task.reset(self.system) self.current_task_number=0 + self.setup_next_task() return {'success': True} From 2fff83f8813c7a731133dd34f7c177340c6494a3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 17:03:19 -0500 Subject: [PATCH 109/329] More streamlining --- .../xmodule/combined_open_ended_module.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index db69c2dae3..22ac3aa0b7 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -180,18 +180,13 @@ class CombinedOpenEndedModule(XModule): task_xml=self.task_xml[task_number] task_type=self.get_tag_name(task_xml) - if task_type=="selfassessment": - task_descriptor=self_assessment_module.SelfAssessmentDescriptor(self.system) - task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) - task=self_assessment_module.SelfAssessmentModule(self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) - last_response=task.latest_answer() - last_score = task.latest_score() - elif task_type=="openended": - task_descriptor=open_ended_module.OpenEndedDescriptor(self.system) - task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) - task=open_ended_module.OpenEndedModule(self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) - last_response=task.latest_answer() - last_score = task.latest_score() + children=self.child_modules() + + task_descriptor=children['descriptors'][task_type](self.system) + task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) + task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) + last_response=task.latest_answer() + last_score = task.latest_score() return last_response, last_score From 1f1fdc2360bc736faa2cb00805ac8dec41dbbe22 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 17:04:16 -0500 Subject: [PATCH 110/329] Add back in support for reset --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 22ac3aa0b7..9384eb9d27 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -120,7 +120,7 @@ class CombinedOpenEndedModule(XModule): } return children - def setup_next_task(self): + def setup_next_task(self, reset=False): current_task_state=None if len(self.task_states)>self.current_task_number: current_task_state=self.task_states[self.current_task_number] @@ -144,7 +144,7 @@ class CombinedOpenEndedModule(XModule): self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING else: - if self.current_task_number>0: + if self.current_task_number>0 and not reset: current_task_state=self.overwrite_state(current_task_state) self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) @@ -257,7 +257,7 @@ class CombinedOpenEndedModule(XModule): self.state=self.INITIAL for i in xrange(0,len(self.task_xml)): self.current_task_number=i - self.setup_next_task() + self.setup_next_task(reset=True) self.current_task.reset(self.system) self.current_task_number=0 self.setup_next_task() From ccefeccecc65f92ee1d04d407b9a1a8402a2f755 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 17:28:50 -0500 Subject: [PATCH 111/329] Several bug fixes --- .../xmodule/xmodule/combined_open_ended_module.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 9384eb9d27..7b20bf053c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -27,7 +27,7 @@ log = logging.getLogger("mitx.courseware") # Set the default number of max attempts. Should be 1 for production # Set higher for debugging/testing # attempts specified in xml definition overrides this. -MAX_ATTEMPTS = 1 +MAX_ATTEMPTS = 10000 # Set maximum available number of points. # Overriden by max_score specified in xml. @@ -149,6 +149,7 @@ class CombinedOpenEndedModule(XModule): self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) log.debug(self.current_task.get_instance_state()) + log.debug(self.get_instance_state()) return True def get_html(self): @@ -192,15 +193,15 @@ class CombinedOpenEndedModule(XModule): def update_task_states(self): changed=False - self.task_states[len(self.task_states)-1] = self.current_task.get_instance_state() - current_task_state=json.loads(self.task_states[len(self.task_states)-1]) + self.task_states[self.current_task_number] = self.current_task.get_instance_state() + current_task_state=json.loads(self.task_states[self.current_task_number]) if current_task_state['state']==self.DONE: self.current_task_number+=1 - if self.current_task_number==len(self.task_xml): + if self.current_task_number>=(len(self.task_xml)): self.state=self.DONE - self.current_task_number=self.current_task_number-1 + self.current_task_number=len(self.task_xml)-1 else: - self.state=self.INTERMEDIATE_DONE + self.state=self.INITIAL changed=True self.setup_next_task() return changed @@ -259,6 +260,7 @@ class CombinedOpenEndedModule(XModule): self.current_task_number=i self.setup_next_task(reset=True) self.current_task.reset(self.system) + self.task_states[self.current_task_number]=self.current_task.get_instance_state() self.current_task_number=0 self.setup_next_task() return {'success': True} From 9ba57ad5df27810f485f5b45175c7e3d61aeee38 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 17:42:42 -0500 Subject: [PATCH 112/329] Add in correctness display for open ended response --- common/lib/xmodule/xmodule/open_ended_module.py | 8 +++++++- lms/templates/open_ended.html | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index d3d04fe2ec..2f8e49e868 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -629,7 +629,12 @@ class OpenEndedModule(): if self.state != self.INITIAL: latest = self.latest_answer() previous_answer = latest if latest is not None else self.initial_display + feedback = self.latest_feedback() + score= self.latest_score() + correct = "correct" if is_submission_correct else 'incorrect' else: + feedback="" + correct="" previous_answer = self.initial_display context = { @@ -640,8 +645,9 @@ class OpenEndedModule(): 'rows' : 30, 'cols' : 80, 'id' : 'open_ended', - 'msg' : self.latest_feedback(), + 'msg' : feedback, 'child_type' : 'openended', + 'correct' : correct, } html = system.render_template('open_ended.html', context) diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index fbf8d74d16..d98e10276d 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -9,9 +9,9 @@
% if state == 'initial': Unanswered - % elif state == 'done': + % elif state == 'done' and correct=='incorrect': Correct - % elif state == 'incorrect': + % elif state == 'done' and correct=='correct': Incorrect % elif state == 'assessing': Submitted for grading From 41ba6dde3543505cbcaca7c7848a745a67fc896d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 18:47:52 -0500 Subject: [PATCH 113/329] Make things looks slightly prettier --- .../xmodule/combined_open_ended_module.py | 2 + .../css/combinedopenended/display.scss | 510 ++++++++++++++++++ .../js/src/combinedopenended/display.coffee | 9 + .../lib/xmodule/xmodule/open_ended_module.py | 10 +- lms/templates/open_ended.html | 10 +- 5 files changed, 531 insertions(+), 10 deletions(-) create mode 100644 common/lib/xmodule/xmodule/css/combinedopenended/display.scss diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 7b20bf053c..4fcff80214 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -47,6 +47,8 @@ class CombinedOpenEndedModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} js_module_name = "CombinedOpenEnded" + css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} + def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, descriptor, diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss new file mode 100644 index 0000000000..73fa4018d2 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -0,0 +1,510 @@ +h2 { + margin-top: 0; + margin-bottom: 15px; + + &.problem-header { + section.staff { + margin-top: 30px; + font-size: 80%; + } + } + + @media print { + display: block; + width: auto; + border-right: 0; + } +} + +.inline-error { + color: darken($error-red, 10%); +} + +section.open-ended-child { + @media print { + display: block; + width: auto; + padding: 0; + + canvas, img { + page-break-inside: avoid; + } + } + + .inline { + display: inline; + } + + ol.enumerate { + li { + &:before { + content: " "; + display: block; + height: 0; + visibility: hidden; + } + } + } + + .solution-span { + > span { + margin: 20px 0; + display: block; + border: 1px solid #ddd; + padding: 9px 15px 20px; + background: #FFF; + position: relative; + @include box-shadow(inset 0 0 0 1px #eee); + @include border-radius(3px); + + &:empty { + display: none; + } + } + } + + div { + p { + &.answer { + margin-top: -2px; + } + &.status { + text-indent: -9999px; + margin: 8px 0 0 10px; + } + } + + &.unanswered { + p.status { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + width: 14px; + } + } + + &.correct, &.ui-icon-check { + p.status { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + width: 25px; + } + + input { + border-color: green; + } + } + + &.processing { + p.status { + @include inline-block(); + background: url('../images/spinner.gif') center center no-repeat; + height: 20px; + width: 20px; + } + + input { + border-color: #aaa; + } + } + + &.incorrect, &.ui-icon-close { + p.status { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + text-indent: -9999px; + } + + input { + border-color: red; + } + } + + > span { + display: block; + margin-bottom: lh(.5); + } + + p.answer { + @include inline-block(); + margin-bottom: 0; + margin-left: 10px; + + &:before { + content: "Answer: "; + font-weight: bold; + display: inline; + + } + &:empty { + &:before { + display: none; + } + } + } + + span { + &.unanswered, &.ui-icon-bullet { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + position: relative; + top: 4px; + width: 14px; + } + + &.processing, &.ui-icon-processing { + @include inline-block(); + background: url('../images/spinner.gif') center center no-repeat; + height: 20px; + position: relative; + top: 6px; + width: 25px; + } + + &.correct, &.ui-icon-check { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + position: relative; + top: 6px; + width: 25px; + } + + &.incorrect, &.ui-icon-close { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + position: relative; + top: 6px; + } + } + + .reload + { + float:right; + margin: 10px; + } + + + .grader-status { + padding: 9px; + background: #F6F6F6; + border: 1px solid #ddd; + border-top: 0; + margin-bottom: 20px; + @include clearfix; + + span { + text-indent: -9999px; + overflow: hidden; + display: block; + float: left; + margin: -7px 7px 0 0; + } + + .grading { + background: url('../images/info-icon.png') left center no-repeat; + padding-left: 25px; + text-indent: 0px; + margin: 0px 7px 0 0; + } + + p { + line-height: 20px; + text-transform: capitalize; + margin-bottom: 0; + float: left; + } + + &.file { + background: #FFF; + margin-top: 20px; + padding: 20px 0 0 0; + + border: { + top: 1px solid #eee; + right: 0; + bottom: 0; + left: 0; + } + + p.debug { + display: none; + } + + input { + float: left; + } + } + + } + .evaluation { + p { + margin-bottom: 4px; + } + } + + + .feedback-on-feedback { + height: 100px; + margin-right: 20px; + } + + .evaluation-response { + header { + text-align: right; + a { + font-size: .85em; + } + } + } + + .evaluation-scoring { + .scoring-list { + list-style-type: none; + margin-left: 3px; + + li { + &:first-child { + margin-left: 0px; + } + display:inline; + margin-left: 50px; + + label { + font-size: .9em; + } + + } + } + + } + .submit-message-container { + margin: 10px 0px ; + } + } + + form.option-input { + margin: -10px 0 20px; + padding-bottom: 20px; + + select { + margin-right: flex-gutter(); + } + } + + ul { + list-style: disc outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } + + ol { + list-style: decimal outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } + + dl { + line-height: 1.4em; + } + + dl dt { + font-weight: bold; + } + + dl dd { + margin-bottom: 0; + } + + dd { + margin-left: .5em; + margin-left: .5rem; + } + + li { + line-height: 1.4em; + margin-bottom: lh(.5); + + &:last-child { + margin-bottom: 0; + } + } + + p { + margin-bottom: lh(); + } + + hr { + background: #ddd; + border: none; + clear: both; + color: #ddd; + float: none; + height: 1px; + margin: 0 0 .75rem; + width: 100%; + } + + .hidden { + display: none; + visibility: hidden; + } + + #{$all-text-inputs} { + display: inline; + width: auto; + } + + section.action { + margin-top: 20px; + + input.save { + @extend .blue-button; + } + + .submission_feedback { + // background: #F3F3F3; + // border: 1px solid #ddd; + // @include border-radius(3px); + // padding: 8px 12px; + // margin-top: 10px; + @include inline-block; + font-style: italic; + margin: 8px 0 0 10px; + color: #777; + -webkit-font-smoothing: antialiased; + } + } + + .detailed-solution { + > p:first-child { + font-size: 0.9em; + font-weight: bold; + font-style: normal; + text-transform: uppercase; + color: #AAA; + } + + p:last-child { + margin-bottom: 0; + } + } + + div.capa_alert { + padding: 8px 12px; + border: 1px solid #EBE8BF; + border-radius: 3px; + background: #FFFCDD; + font-size: 0.9em; + margin-top: 10px; + } + + div.capa_reset { + padding: 25px; + border: 1px solid $error-red; + background-color: lighten($error-red, 25%); + border-radius: 3px; + font-size: 1em; + margin-top: 10px; + margin-bottom: 10px; + } + .capa_reset>h2 { + color: #AA0000; + } + .capa_reset li { + font-size: 0.9em; + } + + .external-grader-message { + section { + padding-left: 20px; + background-color: #FAFAFA; + color: #2C2C2C; + font-family: monospace; + font-size: 1em; + padding-top: 10px; + header { + font-size: 1.4em; + } + + .shortform { + font-weight: bold; + } + + .longform { + padding: 0px; + margin: 0px; + + .result-errors { + margin: 5px; + padding: 10px 10px 10px 40px; + background: url('../images/incorrect-icon.png') center left no-repeat; + li { + color: #B00; + } + } + + .result-output { + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + h4 { + font-family: monospace; + font-size: 1em; + } + + dl { + margin: 0px; + } + + dt { + margin-top: 20px; + } + + dd { + margin-left: 24pt; + } + } + + .result-correct { + background: url('../images/correct-icon.png') left 20px no-repeat; + .result-actual-output { + color: #090; + } + } + + .result-incorrect { + background: url('../images/incorrect-icon.png') left 20px no-repeat; + .result-actual-output { + color: #B00; + } + } + + .markup-text{ + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + bs { + color: #BB0000; + } + + bg { + color: #BDA046; + } + } + } + } + } +} diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index d02db0b932..694b0ae481 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -26,6 +26,8 @@ class @CombinedOpenEnded @submit_button = @$('.submit-button') @child_state = @el.data('state') @child_type = @el.data('child-type') + if @child_type="openended" + @reload_button = @$('.reload-button') @open_ended_child= @$('.open-ended-child') @@ -45,6 +47,8 @@ class @CombinedOpenEnded @reset_button.hide() @next_problem_button.hide() @hint_area.attr('disabled', false) + if @child_type=="openended" + @reload_button.hide() if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') @@ -53,7 +57,12 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit assessment') @submit_button.click @save_assessment + if @child_type == "openended" + @submit_button.hide() + @reload_button.show() else if @child_state == 'post_assessment' + if @child_type=="openended" + @reload_button.hide() @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') if @child_type=="selfassessment" diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 2f8e49e868..5bd17c220a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -474,8 +474,10 @@ class OpenEndedModule(): return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} def is_submission_correct(self, score): - score_ratio = int(score) / float(self.max_score) - correct = (score_ratio >= 0.66) + correct=False + if(isinstance(score,(int, long, float, complex))): + score_ratio = int(score) / float(self.max_score) + correct = (score_ratio >= 0.66) return correct def handle_ajax(self, dispatch, get, system): @@ -631,7 +633,7 @@ class OpenEndedModule(): previous_answer = latest if latest is not None else self.initial_display feedback = self.latest_feedback() score= self.latest_score() - correct = "correct" if is_submission_correct else 'incorrect' + correct = 'correct' if self.is_submission_correct(score) else 'incorrect' else: feedback="" correct="" @@ -649,7 +651,7 @@ class OpenEndedModule(): 'child_type' : 'openended', 'correct' : correct, } - + log.debug(context) html = system.render_template('open_ended.html', context) return html diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index d98e10276d..b29c915d53 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -3,15 +3,15 @@
${prompt|n} - +
% if state == 'initial': Unanswered - % elif state == 'done' and correct=='incorrect': + % elif state in ['done', 'post_assessment'] and correct == 'incorrect': Correct - % elif state == 'done' and correct=='correct': + % elif state in ['done', 'post_assessment'] and correct == 'correct': Incorrect % elif state == 'assessing': Submitted for grading @@ -26,9 +26,7 @@ - % if state == 'assessing': - - % endif +
${msg|n} % if state == 'post_assessment': From e3da5aeb2c7e587805e15a7f209b3e457942ab96 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 18:48:40 -0500 Subject: [PATCH 114/329] Reverse correct and incorrect --- lms/templates/open_ended.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index b29c915d53..423b2dc709 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -9,9 +9,9 @@
% if state == 'initial': Unanswered - % elif state in ['done', 'post_assessment'] and correct == 'incorrect': - Correct % elif state in ['done', 'post_assessment'] and correct == 'correct': + Correct + % elif state in ['done', 'post_assessment'] and correct == 'incorrect': Incorrect % elif state == 'assessing': Submitted for grading From 74222f9ef6f6a5088b862de66932d1584167b6b9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 4 Jan 2013 18:55:57 -0500 Subject: [PATCH 115/329] Fix silly mistake --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 694b0ae481..45d0db8dac 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -26,7 +26,7 @@ class @CombinedOpenEnded @submit_button = @$('.submit-button') @child_state = @el.data('state') @child_type = @el.data('child-type') - if @child_type="openended" + if @child_type=="openended" @reload_button = @$('.reload-button') @open_ended_child= @$('.open-ended-child') From 80763c9baefd39e3bd9eaa846b3897a82a86ded2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 09:44:22 -0500 Subject: [PATCH 116/329] Remove some unneeded imports --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 1 - common/lib/xmodule/xmodule/open_ended_module.py | 1 - 2 files changed, 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4fcff80214..0c9ddd6bb9 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -43,7 +43,6 @@ class CombinedOpenEndedModule(XModule): DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] - #, resource_string(__name__, 'js/src/openended/display.coffee') js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} js_module_name = "CombinedOpenEnded" diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5bd17c220a..e3e23c46d6 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -25,7 +25,6 @@ from .editing_module import EditingDescriptor from .html_checker import check_html from progress import Progress from .stringify import stringify_children -from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location from capa.util import * From 4b7195f21ee6b3561fe1a6dc0a85f521b23d97c9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:01:29 -0500 Subject: [PATCH 117/329] Fix location problem --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 1 + common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 0c9ddd6bb9..4fddb29bf1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -77,6 +77,7 @@ class CombinedOpenEndedModule(XModule): # element. # Scores are on scale from 0 to max_score system.set('location', location) + log.debug(system.location) self.current_task_number = instance_state.get('current_task_number', 0) self.task_states= instance_state.get('task_states', []) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e3e23c46d6..e0911b6489 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -179,9 +179,9 @@ class OpenEndedModule(): self.initial_display = find_with_default(oeparam, 'initial_display', '') self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') - + parsed_grader_payload.update({ - 'location' : system.location, + 'location' : system.location.url(), 'course_id' : system.course_id, 'prompt' : prompt_string, 'rubric' : rubric_string, From d8f6e1fe341b980143535411d2ac476bd45bb6c1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:13:59 -0500 Subject: [PATCH 118/329] Factor out init modules, add in status display --- .../xmodule/combined_open_ended_module.py | 3 + .../lib/xmodule/xmodule/open_ended_module.py | 87 +----------- common/lib/xmodule/xmodule/openendedchild.py | 130 ++++++++++++++++++ .../xmodule/xmodule/self_assessment_module.py | 83 +---------- lms/templates/combined_open_ended.html | 3 + lms/templates/combined_open_ended_status.html | 3 + 6 files changed, 146 insertions(+), 163 deletions(-) create mode 100644 common/lib/xmodule/xmodule/openendedchild.py create mode 100644 lms/templates/combined_open_ended_status.html diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4fddb29bf1..ecdc67a120 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -283,6 +283,9 @@ class CombinedOpenEndedModule(XModule): return json.dumps(state) + def get_status(self): + pass + class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e0911b6489..174fa5e326 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -28,6 +28,7 @@ from .stringify import stringify_children from .xml_module import XmlDescriptor from xmodule.modulestore import Location from capa.util import * +import openendedchild from datetime import datetime @@ -42,89 +43,9 @@ MAX_ATTEMPTS = 1 # Overriden by max_score specified in xml. MAX_SCORE = 1 -class OpenEndedModule(): - """ - States: - - initial (prompt, textbox shown) - | - assessing (read-only textbox, rubric + assessment input shown) - | - request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) - | - done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows - a reset button that goes back to initial state. Saves previous - submissions too.) - """ - - DEFAULT_QUEUE = 'open-ended' - DEFAULT_MESSAGE_QUEUE = 'open-ended-message' - max_inputfields = 1 - - STATE_VERSION = 1 - - # states - INITIAL = 'initial' - ASSESSING = 'assessing' - POST_ASSESSMENT = 'post_assessment' - DONE = 'done' - - def __init__(self, system, location, definition, descriptor, - instance_state=None, shared_state=None, **kwargs): - """ - Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, - and two optional attributes: - attempts, which should be an integer that defaults to 1. - If it's > 1, the student will be able to re-submit after they see - the rubric. - max_score, which should be an integer that defaults to 1. - It defines the maximum number of points a student can get. Assumed to be integer scale - from 0 to max_score, with an interval of 1. - - Note: all the submissions are stored. - - Sample file: - - - - Insert prompt text here. (arbitrary html) - - - Insert grading rubric here. (arbitrary html) - - - Please enter a hint below: (arbitrary html) - - - Thanks for submitting! (arbitrary html) - - - """ - - # Load instance state - if instance_state is not None: - log.debug(instance_state) - instance_state = json.loads(instance_state) - else: - instance_state = {} - - # History is a list of tuples of (answer, score, hint), where hint may be - # None for any element, and score and hint can be None for the last (current) - # element. - # Scores are on scale from 0 to max_score - self.history = instance_state.get('history', []) - - self.state = instance_state.get('state', 'initial') - - self.created = instance_state.get('created', "False") - - self.attempts = instance_state.get('attempts', 0) - self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) - - # Used for progress / grading. Currently get credit just for - # completion (doesn't matter if you self-assessed correct/incorrect). - self._max_score = int(instance_state.get('max_score', MAX_SCORE)) +class OpenEndedModule(openendedchild.OpenEndedChild): + def setup_response(self, system, location, definition, descriptor): oeparam = definition['oeparam'] prompt = definition['prompt'] rubric = definition['rubric'] @@ -179,7 +100,7 @@ class OpenEndedModule(): self.initial_display = find_with_default(oeparam, 'initial_display', '') self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') - + parsed_grader_payload.update({ 'location' : system.location.url(), 'course_id' : system.course_id, diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py new file mode 100644 index 0000000000..b8162800a9 --- /dev/null +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -0,0 +1,130 @@ +""" +A Self Assessment module that allows students to write open-ended responses, +submit, then see a rubric and rate themselves. Persists student supplied +hints, answers, and assessment judgment (currently only correct/incorrect). +Parses xml definition file--see below for exact format. +""" + +import copy +from fs.errors import ResourceNotFoundError +import itertools +import json +import logging +from lxml import etree +from lxml.html import rewrite_links +from path import path +import os +import sys +import hashlib +import capa.xqueue_interface as xqueue_interface + +from pkg_resources import resource_string + +from .capa_module import only_one, ComplexEncoder +from .editing_module import EditingDescriptor +from .html_checker import check_html +from progress import Progress +from .stringify import stringify_children +from .xml_module import XmlDescriptor +from xmodule.modulestore import Location +from capa.util import * + +from datetime import datetime + +log = logging.getLogger("mitx.courseware") + +# Set the default number of max attempts. Should be 1 for production +# Set higher for debugging/testing +# attempts specified in xml definition overrides this. +MAX_ATTEMPTS = 1 + +# Set maximum available number of points. +# Overriden by max_score specified in xml. +MAX_SCORE = 1 + +class OpenEndedChild(): + """ + States: + + initial (prompt, textbox shown) + | + assessing (read-only textbox, rubric + assessment input shown) + | + request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) + | + done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows + a reset button that goes back to initial state. Saves previous + submissions too.) + """ + + DEFAULT_QUEUE = 'open-ended' + DEFAULT_MESSAGE_QUEUE = 'open-ended-message' + max_inputfields = 1 + + STATE_VERSION = 1 + + # states + INITIAL = 'initial' + ASSESSING = 'assessing' + POST_ASSESSMENT = 'post_assessment' + DONE = 'done' + + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + """ + Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, + and two optional attributes: + attempts, which should be an integer that defaults to 1. + If it's > 1, the student will be able to re-submit after they see + the rubric. + max_score, which should be an integer that defaults to 1. + It defines the maximum number of points a student can get. Assumed to be integer scale + from 0 to max_score, with an interval of 1. + + Note: all the submissions are stored. + + Sample file: + + + + Insert prompt text here. (arbitrary html) + + + Insert grading rubric here. (arbitrary html) + + + Please enter a hint below: (arbitrary html) + + + Thanks for submitting! (arbitrary html) + + + """ + + # Load instance state + if instance_state is not None: + instance_state = json.loads(instance_state) + else: + instance_state = {} + + # History is a list of tuples of (answer, score, hint), where hint may be + # None for any element, and score and hint can be None for the last (current) + # element. + # Scores are on scale from 0 to max_score + self.history = instance_state.get('history', []) + + self.state = instance_state.get('state', 'initial') + + self.created = instance_state.get('created', "False") + + self.attempts = instance_state.get('attempts', 0) + self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) + + # Used for progress / grading. Currently get credit just for + # completion (doesn't matter if you self-assessed correct/incorrect). + self._max_score = int(instance_state.get('max_score', MAX_SCORE)) + + self.setup_response(system, location, definition, descriptor) + + def setup_response(self, system, location, definition, descriptor): + pass \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 64edb2bb8a..6c064bebf8 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -26,6 +26,7 @@ from .stringify import stringify_children from .x_module import XModule from .xml_module import XmlDescriptor from xmodule.modulestore import Location +import openendedchild log = logging.getLogger("mitx.courseware") @@ -38,92 +39,14 @@ MAX_ATTEMPTS = 1 # Overriden by max_score specified in xml. MAX_SCORE = 1 -class SelfAssessmentModule(): - """ - States: - - initial (prompt, textbox shown) - | - assessing (read-only textbox, rubric + assessment input shown) - | - request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) - | - done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows - a reset button that goes back to initial state. Saves previous - submissions too.) - """ - - STATE_VERSION = 1 - - # states - INITIAL = 'initial' - ASSESSING = 'assessing' - REQUEST_HINT = 'post_assessment' - DONE = 'done' - - def __init__(self, system, location, definition, descriptor, - instance_state=None, shared_state=None, **kwargs): - """ - Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, - and two optional attributes: - attempts, which should be an integer that defaults to 1. - If it's > 1, the student will be able to re-submit after they see - the rubric. - max_score, which should be an integer that defaults to 1. - It defines the maximum number of points a student can get. Assumed to be integer scale - from 0 to max_score, with an interval of 1. - - Note: all the submissions are stored. - - Sample file: - - - - Insert prompt text here. (arbitrary html) - - - Insert grading rubric here. (arbitrary html) - - - Please enter a hint below: (arbitrary html) - - - Thanks for submitting! (arbitrary html) - - - """ - - # Load instance state - if instance_state is not None: - instance_state = json.loads(instance_state) - else: - instance_state = {} - - instance_state = self.convert_state_to_current_format(instance_state) - - # History is a list of tuples of (answer, score, hint), where hint may be - # None for any element, and score and hint can be None for the last (current) - # element. - # Scores are on scale from 0 to max_score - self.history = instance_state.get('history', []) - - self.created = instance_state.get('created', "False") - - self.state = instance_state.get('state', 'initial') - - self.attempts = instance_state.get('attempts', 0) - self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) - - # Used for progress / grading. Currently get credit just for - # completion (doesn't matter if you self-assessed correct/incorrect). - self._max_score = int(instance_state.get('max_score', MAX_SCORE)) +class SelfAssessmentModule(openendedchild.OpenEndedChild): + def setup_response(self, system, location, definition, descriptor): self.rubric = definition['rubric'] self.prompt = definition['prompt'] self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] - def latest_answer(self): """None if not available""" if not self.history: diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 5b6823e809..a050dad906 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -6,5 +6,8 @@ + +
${status | n}
+
diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html new file mode 100644 index 0000000000..770870b077 --- /dev/null +++ b/lms/templates/combined_open_ended_status.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file From 5333e5e1fc8195d794f0f6f50fed91c44b6a6eca Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:26:27 -0500 Subject: [PATCH 119/329] Refactoring to open ended child --- .../lib/xmodule/xmodule/open_ended_module.py | 80 ++--------------- common/lib/xmodule/xmodule/openendedchild.py | 82 +++++++++++++++++- .../xmodule/xmodule/self_assessment_module.py | 85 +------------------ 3 files changed, 92 insertions(+), 155 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 174fa5e326..b19ed4aa27 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -69,7 +69,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if self.created=="True" and self.state == self.ASSESSING: self.created="False" - self.get_score(self.latest_answer(), system) + self.send_to_grader(self.latest_answer(), system) self.created="False" def _parse(self, oeparam, prompt, rubric, system): @@ -180,7 +180,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'success' : success, 'msg' : "Successfully submitted your feedback."} - def get_score(self, submission, system): + def send_to_grader(self, submission, system): # Prepare xqueue request #------------------------------------------------------------ @@ -228,7 +228,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' self.record_latest_score(score_msg['score']) - self.record_latest_feedback(score_msg['feedback']) + self.record_latest_post_assessment(score_msg['feedback']) self.state=self.POST_ASSESSMENT return True @@ -461,7 +461,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # add new history element with answer and empty score and hint. self.new_history_entry(get['student_answer']) - self.get_score(get['student_answer'], system) + self.send_to_grader(get['student_answer'], system) self.change_state(self.ASSESSING) return {'success': True,} @@ -496,66 +496,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if self.state == self.DONE: self.attempts += 1 - def get_instance_state(self): - """ - Get the current score and state - """ - - state = { - 'version': self.STATE_VERSION, - 'history': self.history, - 'state': self.state, - 'max_score': self._max_score, - 'attempts': self.attempts, - 'created' : "False", - } - return json.dumps(state) - - def latest_answer(self): - """None if not available""" - if not self.history: - return "" - return self.history[-1].get('answer', "") - - def latest_score(self): - """None if not available""" - if not self.history: - return "" - return self.history[-1].get('score', "") - - def latest_feedback(self): - """None if not available""" - if not self.history: - return "" - return self.history[-1].get('feedback', "") - - def new_history_entry(self, answer): - self.history.append({'answer': answer}) - - def record_latest_score(self, score): - """Assumes that state is right, so we're adding a score to the latest - history element""" - self.history[-1]['score'] = score - - def record_latest_feedback(self, feedback): - """Assumes that state is right, so we're adding a score to the latest - history element""" - self.history[-1]['feedback'] = feedback - - def _allow_reset(self): - """Can the module be reset?""" - return self.state == self.DONE and self.attempts < self.max_attempts - def get_html(self, system): #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() previous_answer = latest if latest is not None else self.initial_display - feedback = self.latest_feedback() + post_assessment = self.latest_post_assessment() score= self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' else: - feedback="" + post_assessment="" correct="" previous_answer = self.initial_display @@ -567,7 +517,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'rows' : 30, 'cols' : 80, 'id' : 'open_ended', - 'msg' : feedback, + 'msg' : post_assessment, 'child_type' : 'openended', 'correct' : correct, } @@ -575,27 +525,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): html = system.render_template('open_ended.html', context) return html - def max_score(self): - """ - Return max_score - """ - return self._max_score - - def get_score_value(self): - """ - Returns the last score in the list - """ - score = self.latest_score() - return {'score': score if score is not None else 0, - 'total': self._max_score} - def get_progress(self): ''' For now, just return last score / max_score ''' if self._max_score > 0: try: - return Progress(self.get_score_value()['score'], self._max_score) + return Progress(self.get_score()['score'], self._max_score) except Exception as err: log.exception("Got bad progress") return None diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index b8162800a9..e8800d27dd 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -127,4 +127,84 @@ class OpenEndedChild(): self.setup_response(system, location, definition, descriptor) def setup_response(self, system, location, definition, descriptor): - pass \ No newline at end of file + pass + + def latest_answer(self): + """None if not available""" + if not self.history: + return "" + return self.history[-1].get('answer', "") + + def latest_score(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('score') + + def latest_post_assessment(self): + """None if not available""" + if not self.history: + return "" + return self.history[-1].get('post_assessment', "") + + def new_history_entry(self, answer): + self.history.append({'answer': answer}) + + def record_latest_score(self, score): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['score'] = score + + def record_latest_post_assessment(self, post_assessment): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['post_assessment'] = post_assessment + + def change_state(self, new_state): + """ + A centralized place for state changes--allows for hooks. If the + current state matches the old state, don't run any hooks. + """ + if self.state == new_state: + return + + self.state = new_state + + if self.state == self.DONE: + self.attempts += 1 + + def get_instance_state(self): + """ + Get the current score and state + """ + + state = { + 'version': self.STATE_VERSION, + 'history': self.history, + 'state': self.state, + 'max_score': self._max_score, + 'attempts': self.attempts, + 'created' : "False", + } + return json.dumps(state) + + def _allow_reset(self): + """Can the module be reset?""" + return self.state == self.DONE and self.attempts < self.max_attempts + + def max_score(self): + """ + Return max_score + """ + return self._max_score + + def get_score(self): + """ + Returns the last score in the list + """ + score = self.latest_score() + return {'score': score if score is not None else 0, + 'total': self._max_score} + + + diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 6c064bebf8..864bfee7f8 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -47,51 +47,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] - def latest_answer(self): - """None if not available""" - if not self.history: - return None - return self.history[-1].get('answer') - - def latest_score(self): - """None if not available""" - if not self.history: - return None - return self.history[-1].get('score') - - def latest_hint(self): - """None if not available""" - if not self.history: - return None - return self.history[-1].get('hint') - - def new_history_entry(self, answer): - self.history.append({'answer': answer}) - - def record_latest_score(self, score): - """Assumes that state is right, so we're adding a score to the latest - history element""" - self.history[-1]['score'] = score - - def record_latest_hint(self, hint): - """Assumes that state is right, so we're adding a score to the latest - history element""" - self.history[-1]['hint'] = hint - - - def change_state(self, new_state): - """ - A centralized place for state changes--allows for hooks. If the - current state matches the old state, don't run any hooks. - """ - if self.state == new_state: - return - - self.state = new_state - - if self.state == self.DONE: - self.attempts += 1 - @staticmethod def convert_state_to_current_format(old_state): """ @@ -138,11 +93,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): student_answers, scores, hints)] return new_state - - def _allow_reset(self): - """Can the module be reset?""" - return self.state == self.DONE and self.attempts < self.max_attempts - def get_html(self, system): #set context variables and render template if self.state != self.INITIAL: @@ -166,20 +116,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): html = system.render_template('self_assessment_prompt.html', context) return html - def max_score(self): - """ - Return max_score - """ - return self._max_score - - def get_score(self): - """ - Returns the last score in the list - """ - score = self.latest_score() - return {'score': score if score is not None else 0, - 'total': self._max_score} - def get_progress(self): ''' For now, just return last score / max_score @@ -207,7 +143,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): handlers = { 'save_answer': self.save_answer, 'save_assessment': self.save_assessment, - 'save_post_assessment': self.save_hint, + 'save_post_assessment': self.save_post_assessment, } if dispatch not in handlers: @@ -261,7 +197,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.state == self.DONE: # display the previous hint - latest = self.latest_hint() + latest = self.latest_post_assessment() hint = latest if latest is not None else '' else: hint = '' @@ -376,7 +312,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): # the same number of hints and answers. return self.out_of_sync_error(get) - self.record_latest_hint(get['hint']) + self.record_latest_post_assessment(get['hint']) self.change_state(self.DONE) return {'success': True, @@ -403,21 +339,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return {'success': True} - def get_instance_state(self): - """ - Get the current score and state - """ - - state = { - 'version': self.STATE_VERSION, - 'history': self.history, - 'state': self.state, - 'max_score': self._max_score, - 'attempts': self.attempts, - 'created' : "False", - } - return json.dumps(state) - class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """ From 24689024fd28c35130d7d4ff22a416155e4c0a57 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:32:59 -0500 Subject: [PATCH 120/329] Refactor open ended module --- .../lib/xmodule/xmodule/open_ended_module.py | 54 ------------------- common/lib/xmodule/xmodule/openendedchild.py | 38 +++++++++++++ .../xmodule/xmodule/self_assessment_module.py | 41 -------------- 3 files changed, 38 insertions(+), 95 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index b19ed4aa27..de24d30502 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -411,7 +411,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): } ''' handlers = { - 'problem_get': self.get_problem, 'save_answer': self.save_answer, 'score_update': self.update_score, 'save_post_assessment' : self.message_post, @@ -429,23 +428,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) - def get_problem(self, get, system): - return self.get_html(system) - - def reset_problem(self, get, system): - self.change_state(self.INITIAL) - return {'success': True} - - def out_of_sync_error(self, get, msg=''): - """ - return dict out-of-sync error message, and also log. - """ - log.warning("Assessment module state out sync. state: %r, get: %r. %s", - self.state, get, msg) - return {'success': False, - 'error': 'The problem state got out-of-sync'} - - def save_answer(self, get, system): if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and @@ -483,19 +465,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return dict() # No AJAX return is needed - def change_state(self, new_state): - """ - A centralized place for state changes--allows for hooks. If the - current state matches the old state, don't run any hooks. - """ - if self.state == new_state: - return - - self.state = new_state - - if self.state == self.DONE: - self.attempts += 1 - def get_html(self, system): #set context variables and render template if self.state != self.INITIAL: @@ -525,29 +494,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): html = system.render_template('open_ended.html', context) return html - def get_progress(self): - ''' - For now, just return last score / max_score - ''' - if self._max_score > 0: - try: - return Progress(self.get_score()['score'], self._max_score) - except Exception as err: - log.exception("Got bad progress") - return None - return None - - def reset(self, system): - """ - If resetting is allowed, reset the state. - - Returns {'success': bool, 'error': msg} - (error only present if not success) - """ - self.change_state(self.INITIAL) - return {'success': True} - - class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index e8800d27dd..e152123c37 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -206,5 +206,43 @@ class OpenEndedChild(): return {'score': score if score is not None else 0, 'total': self._max_score} + def reset(self, system): + """ + If resetting is allowed, reset the state. + + Returns {'success': bool, 'error': msg} + (error only present if not success) + """ + self.change_state(self.INITIAL) + return {'success': True} + + def get_progress(self): + ''' + For now, just return last score / max_score + ''' + if self._max_score > 0: + try: + return Progress(self.get_score()['score'], self._max_score) + except Exception as err: + log.exception("Got bad progress") + return None + return None + + def out_of_sync_error(self, get, msg=''): + """ + return dict out-of-sync error message, and also log. + """ + log.warning("Assessment module state out sync. state: %r, get: %r. %s", + self.state, get, msg) + return {'success': False, + 'error': 'The problem state got out-of-sync'} + + def get_html(self): + pass + + def handle_ajax(self): + pass + + diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 864bfee7f8..728a2fd0df 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -116,18 +116,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): html = system.render_template('self_assessment_prompt.html', context) return html - def get_progress(self): - ''' - For now, just return last score / max_score - ''' - if self._max_score > 0: - try: - return Progress(self.get_score()['score'], self._max_score) - except Exception as err: - log.exception("Got bad progress") - return None - return None - def handle_ajax(self, dispatch, get, system): """ @@ -158,15 +146,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) - def out_of_sync_error(self, get, msg=''): - """ - return dict out-of-sync error message, and also log. - """ - log.warning("Assessment module state out sync. state: %r, get: %r. %s", - self.state, get, msg) - return {'success': False, - 'error': 'The problem state got out-of-sync'} - def get_rubric_html(self,system): """ Return the appropriate version of the rubric, based on the state. @@ -296,7 +275,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): d['state'] = self.state return d - def save_hint(self, get, system): ''' Save the hint. @@ -320,25 +298,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'allow_reset': self._allow_reset()} - def reset(self, system): - """ - If resetting is allowed, reset the state. - - Returns {'success': bool, 'error': msg} - (error only present if not success) - """ - #if self.state != self.DONE: - # return self.out_of_sync_error(get) - - #if self.attempts > self.max_attempts: - # return { - # 'success': False, - # 'error': 'Too many attempts.' - # } - self.change_state(self.INITIAL) - return {'success': True} - - class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """ From db7bff293e0c97c46321f4c5d17260d78fec1f11 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:34:44 -0500 Subject: [PATCH 121/329] Add in placeholder status --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index ecdc67a120..75613f7ed1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -165,6 +165,7 @@ class CombinedOpenEndedModule(XModule): 'state' : self.state, 'task_count' : len(self.task_xml), 'task_number' : self.current_task_number+1, + 'status' : "temporary status." } html = self.system.render_template('combined_open_ended.html', context) From 74d95cde4db12305a1926a2b34dae114f31dca6f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:36:13 -0500 Subject: [PATCH 122/329] Fix naming bug --- common/lib/xmodule/xmodule/self_assessment_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 728a2fd0df..cd8ef72c6e 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -131,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): handlers = { 'save_answer': self.save_answer, 'save_assessment': self.save_assessment, - 'save_post_assessment': self.save_post_assessment, + 'save_post_assessment': self.save_hint, } if dispatch not in handlers: From 1c6f271bbb73a2e24c396cc542406989f59ba788 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:38:10 -0500 Subject: [PATCH 123/329] Fix naming bug --- common/lib/xmodule/xmodule/self_assessment_module.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index cd8ef72c6e..de0126c3e0 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -160,7 +160,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.state == self.ASSESSING: context['read_only'] = False - elif self.state in (self.REQUEST_HINT, self.DONE): + elif self.state in (self.POST_ASSESSMENT, self.DONE): context['read_only'] = True else: raise ValueError("Illegal state '%r'" % self.state) @@ -184,7 +184,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): context = {'hint_prompt': self.hint_prompt, 'hint': hint} - if self.state == self.REQUEST_HINT: + if self.state == self.POST_ASSESSMENT: context['read_only'] = False elif self.state == self.DONE: context['read_only'] = True @@ -269,7 +269,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): d['message_html'] = self.get_message_html() d['allow_reset'] = self._allow_reset() else: - self.change_state(self.REQUEST_HINT) + self.change_state(self.POST_ASSESSMENT) d['hint_html'] = self.get_hint_html(system) d['state'] = self.state @@ -285,7 +285,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): with the error key only present if success is False and message_html only if True. ''' - if self.state != self.REQUEST_HINT: + if self.state != self.POST_ASSESSMENT: # Note: because we only ask for hints on wrong answers, may not have # the same number of hints and answers. return self.out_of_sync_error(get) From a06680a23b27e58bc8e402170f1ab0306776758c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:48:05 -0500 Subject: [PATCH 124/329] Move status, remove self assessment format conversion --- .../xmodule/xmodule/self_assessment_module.py | 46 ------------------- lms/templates/combined_open_ended.html | 4 +- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index de0126c3e0..c03620e57f 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -47,52 +47,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] - @staticmethod - def convert_state_to_current_format(old_state): - """ - This module used to use a problematic state representation. This method - converts that into the new format. - - Args: - old_state: dict of state, as passed in. May be old. - - Returns: - new_state: dict of new state - """ - if old_state.get('version', 0) == SelfAssessmentModule.STATE_VERSION: - # already current - return old_state - - # for now, there's only one older format. - - new_state = {'version': SelfAssessmentModule.STATE_VERSION} - - def copy_if_present(key): - if key in old_state: - new_state[key] = old_state[key] - - for to_copy in ['attempts', 'state']: - copy_if_present(to_copy) - - # The answers, scores, and hints need to be kept together to avoid them - # getting out of sync. - - # NOTE: Since there's only one problem with a few hundred submissions - # in production so far, not trying to be smart about matching up hints - # and submissions in cases where they got out of sync. - - student_answers = old_state.get('student_answers', []) - scores = old_state.get('scores', []) - hints = old_state.get('hints', []) - - new_state['history'] = [ - {'answer': answer, - 'score': score, - 'hint': hint} - for answer, score, hint in itertools.izip_longest( - student_answers, scores, hints)] - return new_state - def get_html(self, system): #set context variables and render template if self.state != self.INITIAL: diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index a050dad906..84bded0bec 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,5 +1,7 @@
+
${status | n}
+ % for item in items:
${item['content'] | n}
% endfor @@ -7,7 +9,5 @@ -
${status | n}
-
From 0e2355c279b2fb450ec7f14284c926f460d83728 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 13:58:09 -0500 Subject: [PATCH 125/329] Work on templating for status display --- .../xmodule/combined_open_ended_module.py | 18 ++++++++++++++---- common/lib/xmodule/xmodule/openendedchild.py | 3 +++ .../xmodule/xmodule/self_assessment_module.py | 1 - 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 75613f7ed1..7497e5a8b5 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -98,7 +98,9 @@ class CombinedOpenEndedModule(XModule): return tag def overwrite_state(self, current_task_state): - last_response, last_score=self.get_last_response(self.current_task_number-1) + last_response_data=self.get_last_response(self.current_task_number-1) + last_response = last_response_data['response'] + loaded_task_state=json.loads(current_task_state) if loaded_task_state['state']== self.INITIAL: loaded_task_state['state']=self.ASSESSING @@ -139,7 +141,8 @@ class CombinedOpenEndedModule(XModule): self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: - last_response, last_score=self.get_last_response(self.current_task_number-1) + last_response_data =self.get_last_response(self.current_task_number-1) + last_response = last_response_data['response'] current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) @@ -191,8 +194,10 @@ class CombinedOpenEndedModule(XModule): task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) last_response=task.latest_answer() last_score = task.latest_score() + last_post_response = task.latest_post_response() + last_response_dict={'response' : last_response, 'score' : last_score, 'post_response' : post_response, 'type' : task_type} - return last_response, last_score + return last_response_dict def update_task_states(self): changed=False @@ -285,7 +290,12 @@ class CombinedOpenEndedModule(XModule): return json.dumps(state) def get_status(self): - pass + status=[] + for i in xrange(0,self.current_task_number): + task_data = self.get_last_response(i) + status.append(task_data) + context = {'status_list' : status} + status_html = self.system.render_template("combined_open_ended_status.html", context) class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index e152123c37..ebc6e9a9a6 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -243,6 +243,9 @@ class OpenEndedChild(): def handle_ajax(self): pass + def type(self): + pass + diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index c03620e57f..e2651a080e 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -296,7 +296,6 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): 'hintprompt': parse('hintprompt'), } - def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' elt = etree.Element('selfassessment') From c6c77c0cde3efc71145d4d9cc3c98a27740238b2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 14:05:50 -0500 Subject: [PATCH 126/329] Simple status display --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 11 +++++++---- common/lib/xmodule/xmodule/openendedchild.py | 4 ---- lms/templates/combined_open_ended_status.html | 9 ++++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 7497e5a8b5..02d6cab373 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -168,7 +168,7 @@ class CombinedOpenEndedModule(XModule): 'state' : self.state, 'task_count' : len(self.task_xml), 'task_number' : self.current_task_number+1, - 'status' : "temporary status." + 'status' : self.get_status(), } html = self.system.render_template('combined_open_ended.html', context) @@ -194,8 +194,9 @@ class CombinedOpenEndedModule(XModule): task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) last_response=task.latest_answer() last_score = task.latest_score() - last_post_response = task.latest_post_response() - last_response_dict={'response' : last_response, 'score' : last_score, 'post_response' : post_response, 'type' : task_type} + last_post_assessment = task.latest_post_assessment() + max_score = task.max_score() + last_response_dict={'response' : last_response, 'score' : last_score, 'post_assessment' : last_post_assessment, 'type' : task_type, 'max_score' : max_score} return last_response_dict @@ -273,7 +274,6 @@ class CombinedOpenEndedModule(XModule): self.setup_next_task() return {'success': True} - def get_instance_state(self): """ Get the current score and state @@ -293,10 +293,13 @@ class CombinedOpenEndedModule(XModule): status=[] for i in xrange(0,self.current_task_number): task_data = self.get_last_response(i) + task_data.update({'task_number' : i+1}) status.append(task_data) context = {'status_list' : status} status_html = self.system.render_template("combined_open_ended_status.html", context) + return status_html + class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index ebc6e9a9a6..63ab5b806a 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -243,9 +243,5 @@ class OpenEndedChild(): def handle_ajax(self): pass - def type(self): - pass - - diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 770870b077..bd4dce27e5 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -1,3 +1,10 @@
- + %for status in status_list: + + Step ${status['task_number']} : ${status['score']} / ${status['max_score']} + %if status['type']=="openended": + ${status['post_assessment']} + %endif + + %endfor
\ No newline at end of file From 5e38433494db5c41bc50f910df84163011a849bd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 15:11:01 -0500 Subject: [PATCH 127/329] Add in static data for max score and max attempts --- .../xmodule/combined_open_ended_module.py | 20 +++++++++++++------ .../lib/xmodule/xmodule/open_ended_module.py | 14 ------------- common/lib/xmodule/xmodule/openendedchild.py | 6 +++--- .../xmodule/xmodule/self_assessment_module.py | 9 --------- lms/templates/combined_open_ended_status.html | 2 +- 5 files changed, 18 insertions(+), 33 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 02d6cab373..c47919095f 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -90,6 +90,11 @@ class CombinedOpenEndedModule(XModule): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) + self.static_data = { + 'max_score' : self._max_score, + 'max_attempts' : self.max_attempts, + } + self.task_xml=definition['task_xml'] self.setup_next_task() @@ -137,7 +142,7 @@ class CombinedOpenEndedModule(XModule): self.current_task_descriptor=children['descriptors'][current_task_type](self.system) self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) if current_task_state is None and self.current_task_number==0: - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor) + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING elif current_task_state is None and self.current_task_number>0: @@ -145,13 +150,13 @@ class CombinedOpenEndedModule(XModule): last_response = last_response_data['response'] current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) self.state=self.ASSESSING else: if self.current_task_number>0 and not reset: current_task_state=self.overwrite_state(current_task_state) - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, instance_state=current_task_state) + self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) log.debug(self.current_task.get_instance_state()) log.debug(self.get_instance_state()) @@ -191,12 +196,15 @@ class CombinedOpenEndedModule(XModule): task_descriptor=children['descriptors'][task_type](self.system) task_parsed_xml=task_descriptor.definition_from_xml(etree.fromstring(task_xml),self.system) - task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, instance_state=task_state) + task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, self.static_data, instance_state=task_state) last_response=task.latest_answer() last_score = task.latest_score() last_post_assessment = task.latest_post_assessment() max_score = task.max_score() - last_response_dict={'response' : last_response, 'score' : last_score, 'post_assessment' : last_post_assessment, 'type' : task_type, 'max_score' : max_score} + state = task.state + last_response_dict={'response' : last_response, 'score' : last_score, + 'post_assessment' : last_post_assessment, + 'type' : task_type, 'max_score' : max_score, 'state' : state} return last_response_dict @@ -291,7 +299,7 @@ class CombinedOpenEndedModule(XModule): def get_status(self): status=[] - for i in xrange(0,self.current_task_number): + for i in xrange(0,self.current_task_number+1): task_data = self.get_last_response(i) task_data.update({'task_number' : i+1}) status.append(task_data) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index de24d30502..c607e3eccf 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -34,15 +34,6 @@ from datetime import datetime log = logging.getLogger("mitx.courseware") -# Set the default number of max attempts. Should be 1 for production -# Set higher for debugging/testing -# attempts specified in xml definition overrides this. -MAX_ATTEMPTS = 1 - -# Set maximum available number of points. -# Overriden by max_score specified in xml. -MAX_SCORE = 1 - class OpenEndedModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): @@ -113,11 +104,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.payload = {'grader_payload': updated_grader_payload} - try: - self.max_score = int(find_with_default(oeparam, 'max_score', 1)) - except ValueError: - self.max_score = 1 - def message_post(self,get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 63ab5b806a..2e3a5b8e02 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -69,7 +69,7 @@ class OpenEndedChild(): POST_ASSESSMENT = 'post_assessment' DONE = 'done' - def __init__(self, system, location, definition, descriptor, + def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): """ Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, @@ -118,11 +118,11 @@ class OpenEndedChild(): self.created = instance_state.get('created', "False") self.attempts = instance_state.get('attempts', 0) - self.max_attempts = int(instance_state.get('attempts', MAX_ATTEMPTS)) + self.max_attempts = static_data['max_attempts'] # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). - self._max_score = int(instance_state.get('max_score', MAX_SCORE)) + self._max_score = static_data['max_score'] self.setup_response(system, location, definition, descriptor) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index e2651a080e..88c47f92ef 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -30,15 +30,6 @@ import openendedchild log = logging.getLogger("mitx.courseware") -# Set the default number of max attempts. Should be 1 for production -# Set higher for debugging/testing -# attempts specified in xml definition overrides this. -MAX_ATTEMPTS = 1 - -# Set maximum available number of points. -# Overriden by max_score specified in xml. -MAX_SCORE = 1 - class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index bd4dce27e5..e53c8136e3 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -1,7 +1,7 @@
%for status in status_list: - Step ${status['task_number']} : ${status['score']} / ${status['max_score']} + Step ${status['task_number']} (${status['state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended": ${status['post_assessment']} %endif From 1e4c85955a7b6b44a80489f7b7780678c2c5dd89 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 15:12:14 -0500 Subject: [PATCH 128/329] Rename max score --- common/lib/xmodule/xmodule/open_ended_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index c607e3eccf..437346828c 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -196,7 +196,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, - 'max_score' : self.max_score, + 'max_score' : self.max_score(), }) # Submit request. When successful, 'msg' is the prior length of the queue @@ -333,7 +333,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): feedback_template = system.render_template("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], - 'score': "{0} / {1}".format(response_items['score'], self.max_score), + 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, }) @@ -382,7 +382,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def is_submission_correct(self, score): correct=False if(isinstance(score,(int, long, float, complex))): - score_ratio = int(score) / float(self.max_score) + score_ratio = int(score) / float(self.max_score()) correct = (score_ratio >= 0.66) return correct From 0ec1db81693f86c2787a0cb9547d951af381a96a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 15:38:04 -0500 Subject: [PATCH 129/329] Add in a message area --- .../xmodule/js/src/combinedopenended/display.coffee | 12 ++++-------- lms/templates/combined_open_ended.html | 1 - lms/templates/combined_open_ended_status.html | 4 ++-- lms/templates/open_ended.html | 1 + lms/templates/self_assessment_prompt.html | 2 ++ 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 45d0db8dac..4ad0bde631 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -74,13 +74,9 @@ class @CombinedOpenEnded @hint_area.attr('disabled', true) @submit_button.hide() if @task_number<@task_count - @next_problem_button.show() + @next_problem else - @next_problem_button.hide() - #if @allow_reset @reset_button.show() - #else - # @reset_button.hide() find_assessment_elements: -> @@ -161,8 +157,7 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - next_problem: (event) => - event.preventDefault() + next_problem: => if @child_state == 'done' $.postWithPrefix "#{@ajax_url}/next_problem", {}, (response) => if response.success @@ -203,7 +198,7 @@ class @CombinedOpenEnded processData: false contentType: false success: (response) => - @gentle_alert response.message + @gentle_alert response.msg @$('section.evaluation').slideToggle() @message_wrapper.html(response.message_html) @child_state = 'done' @@ -216,4 +211,5 @@ class @CombinedOpenEnded if @el.find('.open-ended-alert').length @el.find('.open-ended-alert').remove() alert_elem = "
" + msg + "
" + @el.find('.open-ended-action').after(alert_elem) @el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) \ No newline at end of file diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 84bded0bec..6b30c3f418 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -8,6 +8,5 @@ -
diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index e53c8136e3..6450f5a43e 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -1,10 +1,10 @@
%for status in status_list: - +
Step ${status['task_number']} (${status['state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended": ${status['post_assessment']} %endif - +
%endfor
\ No newline at end of file diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 423b2dc709..838c847841 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -51,5 +51,6 @@
% endif
+
diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 479e42a126..d8186954a7 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -16,4 +16,6 @@
${initial_message}
+
+
From 146e0919f12f6598d0a1553cb4bd0f56d8a6ec77 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 15:59:13 -0500 Subject: [PATCH 130/329] Work on alerts, transitions between pages --- .../xmodule/xmodule/combined_open_ended_module.py | 2 +- .../xmodule/js/src/combinedopenended/display.coffee | 12 +++++++++--- lms/templates/open_ended.html | 5 +++-- lms/templates/self_assessment_prompt.html | 5 +++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index c47919095f..1761434a09 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -255,7 +255,7 @@ class CombinedOpenEndedModule(XModule): def next_problem(self, get): self.update_task_states() - return {'success' : True} + return {'success' : True, 'html' : self.get_html()} def reset(self, get): """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 4ad0bde631..94cbdd5fe4 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -1,6 +1,11 @@ class @CombinedOpenEnded constructor: (element) -> + @element=element + @reinitialize(element) + + reinitialize: (element) -> @el = $(element).find('section.combined-open-ended') + @combined_open_ended=$(element).find('section.combined-open-ended') @id = @el.data('id') @ajax_url = @el.data('ajax-url') @state = @el.data('state') @@ -12,7 +17,6 @@ class @CombinedOpenEnded @reset_button.click @reset @next_problem_button = @$('.next-step-button') @next_problem_button.click @next_problem - @combined_open_ended= @$('.combined-open-ended') # valid states: 'initial', 'assessing', 'post_assessment', 'done' # Where to put the rubric once we load it @@ -74,7 +78,7 @@ class @CombinedOpenEnded @hint_area.attr('disabled', true) @submit_button.hide() if @task_number<@task_count - @next_problem + @next_problem() else @reset_button.show() @@ -166,9 +170,11 @@ class @CombinedOpenEnded @hint_wrapper.html('') @message_wrapper.html('') @child_state = 'initial' + @combined_open_ended.html(response.html) + @reinitialize(@element) @rebind() @next_problem_button.hide() - location.reload() + @gentle_alert "Moved to next step." else @errors_area.html(response.error) else diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 838c847841..2c0600cee0 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -24,6 +24,9 @@ +
+
+ @@ -51,6 +54,4 @@ % endif -
-
diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index d8186954a7..4ef057fa34 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -9,6 +9,9 @@ +
+
+
${initial_rubric}
${initial_hint}
@@ -16,6 +19,4 @@
${initial_message}
-
-
From 8d6a5c45f31c6b833745265e00901f64cbf619a4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 16:37:51 -0500 Subject: [PATCH 131/329] Try to render without module system --- .../xmodule/combined_open_ended_module.py | 16 +++++++++++++--- .../js/src/combinedopenended/display.coffee | 6 ++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 1761434a09..29c5265792 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -22,6 +22,8 @@ from xmodule.modulestore import Location import self_assessment_module import open_ended_module +from mitxmako.shortcuts import render_to_string + log = logging.getLogger("mitx.courseware") # Set the default number of max attempts. Should be 1 for production @@ -162,7 +164,7 @@ class CombinedOpenEndedModule(XModule): log.debug(self.get_instance_state()) return True - def get_html(self): + def get_context(self): task_html=self.get_html_base() #set context variables and render template @@ -176,9 +178,17 @@ class CombinedOpenEndedModule(XModule): 'status' : self.get_status(), } + return context + + def get_html(self): + context=self.get_context() html = self.system.render_template('combined_open_ended.html', context) return html + def get_html_nonsystem(self): + context=self.get_context() + html = render_to_string('combined_open_ended.html', context) + return html def get_html_base(self): self.update_task_states() @@ -255,7 +265,7 @@ class CombinedOpenEndedModule(XModule): def next_problem(self, get): self.update_task_states() - return {'success' : True, 'html' : self.get_html()} + return {'success' : True, 'html' : self.get_html_nonsystem()} def reset(self, get): """ @@ -280,7 +290,7 @@ class CombinedOpenEndedModule(XModule): self.task_states[self.current_task_number]=self.current_task.get_instance_state() self.current_task_number=0 self.setup_next_task() - return {'success': True} + return {'success': True, 'html' : self.get_html_nonsystem()} def get_instance_state(self): """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 94cbdd5fe4..a12898e14a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -4,6 +4,7 @@ class @CombinedOpenEnded @reinitialize(element) reinitialize: (element) -> + @wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule') @el = $(element).find('section.combined-open-ended') @combined_open_ended=$(element).find('section.combined-open-ended') @id = @el.data('id') @@ -153,9 +154,10 @@ class @CombinedOpenEnded @hint_wrapper.html('') @message_wrapper.html('') @child_state = 'initial' + @combined_open_ended.after(response.html).remove() + @reinitialize(@element) @rebind() @reset_button.hide() - location.reload() else @errors_area.html(response.error) else @@ -170,7 +172,7 @@ class @CombinedOpenEnded @hint_wrapper.html('') @message_wrapper.html('') @child_state = 'initial' - @combined_open_ended.html(response.html) + @combined_open_ended.after(response.html).remove() @reinitialize(@element) @rebind() @next_problem_button.hide() From 792e57ac62fb4d5ba789cb269d438b682da6e884 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 16:40:11 -0500 Subject: [PATCH 132/329] Remove check for feedback button --- .../xmodule/js/src/combinedopenended/display.coffee | 7 ------- lms/templates/open_ended.html | 1 - 2 files changed, 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a12898e14a..b9dd460b5f 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -31,8 +31,6 @@ class @CombinedOpenEnded @submit_button = @$('.submit-button') @child_state = @el.data('state') @child_type = @el.data('child-type') - if @child_type=="openended" - @reload_button = @$('.reload-button') @open_ended_child= @$('.open-ended-child') @@ -52,8 +50,6 @@ class @CombinedOpenEnded @reset_button.hide() @next_problem_button.hide() @hint_area.attr('disabled', false) - if @child_type=="openended" - @reload_button.hide() if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') @@ -64,10 +60,7 @@ class @CombinedOpenEnded @submit_button.click @save_assessment if @child_type == "openended" @submit_button.hide() - @reload_button.show() else if @child_state == 'post_assessment' - if @child_type=="openended" - @reload_button.hide() @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') if @child_type=="selfassessment" diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 2c0600cee0..93e931fa83 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -29,7 +29,6 @@ -
${msg|n} % if state == 'post_assessment': From b6774ed2e8058069e604a5e4e4a187d547dea541 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 16:44:10 -0500 Subject: [PATCH 133/329] Add back check for feedback button. Can always remove later --- .../xmodule/js/src/combinedopenended/display.coffee | 7 +++++++ lms/templates/open_ended.html | 1 + 2 files changed, 8 insertions(+) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index b9dd460b5f..a12898e14a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -31,6 +31,8 @@ class @CombinedOpenEnded @submit_button = @$('.submit-button') @child_state = @el.data('state') @child_type = @el.data('child-type') + if @child_type=="openended" + @reload_button = @$('.reload-button') @open_ended_child= @$('.open-ended-child') @@ -50,6 +52,8 @@ class @CombinedOpenEnded @reset_button.hide() @next_problem_button.hide() @hint_area.attr('disabled', false) + if @child_type=="openended" + @reload_button.hide() if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') @@ -60,7 +64,10 @@ class @CombinedOpenEnded @submit_button.click @save_assessment if @child_type == "openended" @submit_button.hide() + @reload_button.show() else if @child_state == 'post_assessment' + if @child_type=="openended" + @reload_button.hide() @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') if @child_type=="selfassessment" diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 93e931fa83..2c0600cee0 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -29,6 +29,7 @@ +
${msg|n} % if state == 'post_assessment': From a7c64b8e2d16a23d19bac48e95778309b9fe39c0 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:06:52 -0500 Subject: [PATCH 134/329] Add collapsible --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 4 +++- lms/templates/open_ended.html | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 29c5265792..5f68ab411f 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -45,7 +45,9 @@ class CombinedOpenEndedModule(XModule): DONE = 'done' TASK_TYPES=["self", "ml", "instructor", "peer"] - js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee')]} + js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee'), + ]} js_module_name = "CombinedOpenEnded" css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 2c0600cee0..31599ecea5 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -1,12 +1,12 @@
-
${prompt|n}
-
+
+
% if state == 'initial': Unanswered % elif state in ['done', 'post_assessment'] and correct == 'correct': From c7f0ba7bc45940c46c82ca3c275705a093d4e93c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:11:04 -0500 Subject: [PATCH 135/329] Fix some of the styling issues --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 1 + .../xmodule/xmodule/css/combinedopenended/display.scss | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 5f68ab411f..2c2e9b57f5 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -47,6 +47,7 @@ class CombinedOpenEndedModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), + resource_string(__name__, 'js/src/javascript_loader.coffee'), ]} js_module_name = "CombinedOpenEnded" diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 73fa4018d2..c13a975c85 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -63,7 +63,6 @@ section.open-ended-child { } } - div { p { &.answer { margin-top: -2px; @@ -74,7 +73,7 @@ section.open-ended-child { } } - &.unanswered { + div.unanswered { p.status { @include inline-block(); background: url('../images/unanswered-icon.png') center center no-repeat; @@ -83,7 +82,7 @@ section.open-ended-child { } } - &.correct, &.ui-icon-check { + div.correct, div.ui-icon-check { p.status { @include inline-block(); background: url('../images/correct-icon.png') center center no-repeat; @@ -96,7 +95,7 @@ section.open-ended-child { } } - &.processing { + div.processing { p.status { @include inline-block(); background: url('../images/spinner.gif') center center no-repeat; @@ -109,7 +108,7 @@ section.open-ended-child { } } - &.incorrect, &.ui-icon-close { + div.incorrect, div.ui-icon-close { p.status { @include inline-block(); background: url('../images/incorrect-icon.png') center center no-repeat; @@ -287,7 +286,6 @@ section.open-ended-child { .submit-message-container { margin: 10px 0px ; } - } form.option-input { margin: -10px 0 20px; From 3ea59ef3fefc2b185ecdc75f584b3787ae5d85ff Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:15:08 -0500 Subject: [PATCH 136/329] set collapsible elements --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a12898e14a..cfa2ef8331 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -19,6 +19,7 @@ class @CombinedOpenEnded @next_problem_button = @$('.next-step-button') @next_problem_button.click @next_problem # valid states: 'initial', 'assessing', 'post_assessment', 'done' + Collapsible.setCollapsibles(@el) # Where to put the rubric once we load it @el = $(element).find('section.open-ended-child') From c2a285e0c7a251cf7871a41b3fb9f82d719f5778 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:20:59 -0500 Subject: [PATCH 137/329] store feedback in dictionary and only parse when needed --- .../lib/xmodule/xmodule/open_ended_module.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 437346828c..5eaf3f94b7 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -30,6 +30,8 @@ from xmodule.modulestore import Location from capa.util import * import openendedchild +from mitxmako.shortcuts import render_to_string + from datetime import datetime log = logging.getLogger("mitx.courseware") @@ -209,8 +211,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return True def _update_score(self, score_msg, queuekey, system): - score_msg = self._parse_score_msg(score_msg, system) - if not score_msg['valid']: + new_score_msg = self._parse_score_msg(score_msg) + if not new_score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' self.record_latest_score(score_msg['score']) @@ -317,7 +319,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return u"\n".join([feedback_list_part1,feedback_list_part2]) - def _format_feedback(self, response_items, system): + def _format_feedback(self, response_items): """ Input: Dictionary called feedback. Must contain keys seen below. @@ -331,7 +333,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return system.render_template("open_ended_error.html", {'errors' : feedback}) - feedback_template = system.render_template("open_ended_feedback.html", { + feedback_template = render_to_string("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, @@ -340,7 +342,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return feedback_template - def _parse_score_msg(self, score_msg, system): + def _parse_score_msg(self, score_msg): """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, @@ -373,12 +375,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): .format(tag)) return fail - feedback = self._format_feedback(score_result, system) + feedback = self._format_feedback(score_result) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} + def latest_post_assessment(self): + """None if not available""" + if not self.history: + return "" + return self._parse_score_msg(self.history[-1].get('post_assessment', "")) + def is_submission_correct(self, score): correct=False if(isinstance(score,(int, long, float, complex))): From 573ec68abe73deb33504854c5b7969269c2bd051 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:29:10 -0500 Subject: [PATCH 138/329] Store feedback as dictionary to be parsed later --- common/lib/xmodule/xmodule/open_ended_module.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5eaf3f94b7..351f098c50 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -215,8 +215,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not new_score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' - self.record_latest_score(score_msg['score']) - self.record_latest_post_assessment(score_msg['feedback']) + self.record_latest_score(new_score_msg['score']) + self.record_latest_post_assessment(score_msg) self.state=self.POST_ASSESSMENT return True @@ -385,7 +385,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """None if not available""" if not self.history: return "" - return self._parse_score_msg(self.history[-1].get('post_assessment', "")) + feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', "")) + return feedback_dict['feedback'] if feedback_dict['valid'] else '' def is_submission_correct(self, score): correct=False From d9ea0fcda06712c65bb10f01d217f0ee55af6893 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:47:34 -0500 Subject: [PATCH 139/329] display feedback in status box --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 4 +++- common/lib/xmodule/xmodule/open_ended_module.py | 9 +++++++-- common/lib/xmodule/xmodule/openendedchild.py | 7 +++++++ lms/templates/combined_open_ended_status.html | 6 ++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 2c2e9b57f5..b0fb365d73 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -213,11 +213,13 @@ class CombinedOpenEndedModule(XModule): last_response=task.latest_answer() last_score = task.latest_score() last_post_assessment = task.latest_post_assessment() + if task_type=="openended": + last_post_assessment = task.latest_post_assessment(short_feedback=True) max_score = task.max_score() state = task.state last_response_dict={'response' : last_response, 'score' : last_score, 'post_assessment' : last_post_assessment, - 'type' : task_type, 'max_score' : max_score, 'state' : state} + 'type' : task_type, 'max_score' : max_score, 'state' : state, 'human_state' : task.HUMAN_NAMES[state]} return last_response_dict diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 351f098c50..c5cc01964e 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -381,12 +381,17 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} - def latest_post_assessment(self): + def latest_post_assessment(self, short_feedback=False): """None if not available""" if not self.history: return "" + feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', "")) - return feedback_dict['feedback'] if feedback_dict['valid'] else '' + if not short_feedback: + return feedback_dict['feedback'] if feedback_dict['valid'] else '' + + short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) + return short_feedback if feedback_dict['valid'] else '' def is_submission_correct(self, score): correct=False diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 2e3a5b8e02..236bd03c4c 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -69,6 +69,13 @@ class OpenEndedChild(): POST_ASSESSMENT = 'post_assessment' DONE = 'done' + HUMAN_NAMES={ + 'initial' : 'Started', + 'assessing' : 'Being scored', + 'post_assessment' : 'Scoring finished', + 'done' : 'Problem complete', + } + def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): """ diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 6450f5a43e..5b8ac5fcbe 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -1,9 +1,11 @@
%for status in status_list:
- Step ${status['task_number']} (${status['state']}) : ${status['score']} / ${status['max_score']} + Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended": - ${status['post_assessment']} +
+ ${status['post_assessment']} +
%endif
%endfor From ea6945a3fe1bffd84ef3a5572fcd96f0af52ff04 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:50:30 -0500 Subject: [PATCH 140/329] Collapsible feedback --- lms/templates/combined_open_ended_status.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 5b8ac5fcbe..529d8dcf15 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -4,7 +4,12 @@ Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended":
%endif
From d4dd0cd84c930e80931603491d63807cd479fbd3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 17:55:38 -0500 Subject: [PATCH 141/329] Html changes --- common/lib/xmodule/xmodule/js/src/collapsible.coffee | 2 +- lms/templates/combined_open_ended_status.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/collapsible.coffee b/common/lib/xmodule/xmodule/js/src/collapsible.coffee index 18a186e106..e414935784 100644 --- a/common/lib/xmodule/xmodule/js/src/collapsible.coffee +++ b/common/lib/xmodule/xmodule/js/src/collapsible.coffee @@ -22,7 +22,7 @@ class @Collapsible if $(event.target).text() == 'See full output' new_text = 'Hide output' else - new_text = 'See full ouput' + new_text = 'See full output' $(event.target).text(new_text) @toggleHint: (event) => diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 529d8dcf15..41ec403c47 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -5,7 +5,7 @@ %if status['type']=="openended":
- Show Feedback + Show feedback from step ${status['task_number']}
${status['post_assessment']} From 8d80fcfb8694237c11a95fda8631d16ec368229e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:05:19 -0500 Subject: [PATCH 142/329] Add button to skip post assessment --- .../xmodule/js/src/combinedopenended/display.coffee | 4 ++++ common/lib/xmodule/xmodule/open_ended_module.py | 9 +++++++-- lms/templates/open_ended.html | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index cfa2ef8331..ca2b707629 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -34,6 +34,7 @@ class @CombinedOpenEnded @child_type = @el.data('child-type') if @child_type=="openended" @reload_button = @$('.reload-button') + @skip_button = @$('.skip-button') @open_ended_child= @$('.open-ended-child') @@ -52,6 +53,7 @@ class @CombinedOpenEnded @submit_button.show() @reset_button.hide() @next_problem_button.hide() + @skip_button.hide() @hint_area.attr('disabled', false) if @child_type=="openended" @reload_button.hide() @@ -69,6 +71,7 @@ class @CombinedOpenEnded else if @child_state == 'post_assessment' if @child_type=="openended" @reload_button.hide() + @skip_button.show() @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') if @child_type=="selfassessment" @@ -79,6 +82,7 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() + @skip_button.hide() if @task_number<@task_count @next_problem() else diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index c5cc01964e..e1a34d284b 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -106,6 +106,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.payload = {'grader_payload': updated_grader_payload} + def skip_post_assessment(self, get, system): + self.state=self.DONE + return {'success' : True} + def message_post(self,get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) @@ -389,8 +393,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', "")) if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' - - short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) + if feedback_dict['valid']: + short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' def is_submission_correct(self, score): @@ -414,6 +418,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'save_answer': self.save_answer, 'score_update': self.update_score, 'save_post_assessment' : self.message_post, + 'skip_post_assessment' : self.skip_post_assessment(), } if dispatch not in handlers: diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 31599ecea5..1235014f82 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -28,7 +28,7 @@
- +
${msg|n} From ea5db36e927819a9705c982ced120c5aa7054488 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:08:48 -0500 Subject: [PATCH 143/329] Skip post assessment button --- .../js/src/combinedopenended/display.coffee | 14 ++++++++++++++ common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index ca2b707629..74a30d05a1 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -35,6 +35,7 @@ class @CombinedOpenEnded if @child_type=="openended" @reload_button = @$('.reload-button') @skip_button = @$('.skip-button') + @skip_button.click @skip_post_assessment() @open_ended_child= @$('.open-ended-child') @@ -148,6 +149,19 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') + skip_post_assessment: (event) => + event.preventDefault() + if @child_state == 'post_assessment' + + $.postWithPrefix "#{@ajax_url}/skip_post_assessment", data, (response) => + if response.success + @child_state = 'done' + @allow_reset = response.allow_reset + @rebind() + else + @errors_area.html(response.error) + else + @errors_area.html('Problem state got out of sync. Try reloading the page.') reset: (event) => event.preventDefault() diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e1a34d284b..b1931f81be 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -418,7 +418,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'save_answer': self.save_answer, 'score_update': self.update_score, 'save_post_assessment' : self.message_post, - 'skip_post_assessment' : self.skip_post_assessment(), + 'skip_post_assessment' : self.skip_post_assessment, } if dispatch not in handlers: From e46ecdb6e2296f18589ffa8d47af5c46e788abe2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:10:00 -0500 Subject: [PATCH 144/329] Move skip button --- lms/templates/open_ended.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 1235014f82..e995d62cb1 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -23,12 +23,13 @@
+
- +
${msg|n} From bd5ca9d2a7fb35cbef1822d40a59c21abc86d396 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:11:02 -0500 Subject: [PATCH 145/329] JS bug fix --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 74a30d05a1..187366d3a6 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -35,7 +35,7 @@ class @CombinedOpenEnded if @child_type=="openended" @reload_button = @$('.reload-button') @skip_button = @$('.skip-button') - @skip_button.click @skip_post_assessment() + @skip_button.click @skip_post_assessment @open_ended_child= @$('.open-ended-child') From f5cd2ff9c557dd9e4bfb4908d970111c275a55bc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:13:03 -0500 Subject: [PATCH 146/329] JS bugfix --- .../lib/xmodule/xmodule/js/src/combinedopenended/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 187366d3a6..ff06e6a850 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -153,7 +153,7 @@ class @CombinedOpenEnded event.preventDefault() if @child_state == 'post_assessment' - $.postWithPrefix "#{@ajax_url}/skip_post_assessment", data, (response) => + $.postWithPrefix "#{@ajax_url}/skip_post_assessment", {}, (response) => if response.success @child_state = 'done' @allow_reset = response.allow_reset From 9a4ee350d7e4e9facff77e6370e7b0814f0faabc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:21:02 -0500 Subject: [PATCH 147/329] JS tweaks, move feedback into its own template --- .../js/src/combinedopenended/display.coffee | 9 ++++---- lms/templates/combined_open_ended_status.html | 2 +- lms/templates/open_ended.html | 23 +------------------ lms/templates/open_ended_evaluation.html | 22 ++++++++++++++++++ 4 files changed, 29 insertions(+), 27 deletions(-) create mode 100644 lms/templates/open_ended_evaluation.html diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index ff06e6a850..f6ad7eb22a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -54,10 +54,10 @@ class @CombinedOpenEnded @submit_button.show() @reset_button.hide() @next_problem_button.hide() - @skip_button.hide() @hint_area.attr('disabled', false) if @child_type=="openended" @reload_button.hide() + @skip_button.hide() if @child_state == 'initial' @answer_area.attr("disabled", false) @submit_button.prop('value', 'Submit') @@ -73,6 +73,7 @@ class @CombinedOpenEnded if @child_type=="openended" @reload_button.hide() @skip_button.show() + @skip_post_assessment() @answer_area.attr("disabled", true) @submit_button.prop('value', 'Submit post-assessment') if @child_type=="selfassessment" @@ -83,7 +84,8 @@ class @CombinedOpenEnded @answer_area.attr("disabled", true) @hint_area.attr('disabled', true) @submit_button.hide() - @skip_button.hide() + if @child_type=="openended" + @skip_button.hide() if @task_number<@task_count @next_problem() else @@ -149,8 +151,7 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - skip_post_assessment: (event) => - event.preventDefault() + skip_post_assessment: => if @child_state == 'post_assessment' $.postWithPrefix "#{@ajax_url}/skip_post_assessment", {}, (response) => diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 41ec403c47..7bc66d954f 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -3,7 +3,7 @@
Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended": -
+
Show feedback from step ${status['task_number']}
diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index e995d62cb1..9ade05d0e2 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -32,27 +32,6 @@
- ${msg|n} - % if state == 'post_assessment': -
-
- Respond to Feedback -
-
-

How accurate do you find this feedback?

-
-
    -
  • -
  • -
  • -
  • -
  • -
-
-

Additional comments:

- -
-
- % endif + ${msg|n}
diff --git a/lms/templates/open_ended_evaluation.html b/lms/templates/open_ended_evaluation.html new file mode 100644 index 0000000000..018486a1d6 --- /dev/null +++ b/lms/templates/open_ended_evaluation.html @@ -0,0 +1,22 @@ +
+ ${msg|n} +
+
+ Respond to Feedback +
+
+

How accurate do you find this feedback?

+
+
    +
  • +
  • +
  • +
  • +
  • +
+
+

Additional comments:

+ +
+
+
\ No newline at end of file From f287752fcbf548ebe90eb2c42c0d1bf02dd8a1c4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:23:24 -0500 Subject: [PATCH 148/329] CSS tweaks --- common/lib/xmodule/xmodule/css/combinedopenended/display.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index c13a975c85..faa3c79419 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -20,7 +20,7 @@ h2 { color: darken($error-red, 10%); } -section.open-ended-child { +section.open-ended-child, section.combined-open-ended-status { @media print { display: block; width: auto; @@ -396,7 +396,7 @@ section.open-ended-child { } } - div.capa_alert { + div.open-ended-alert { padding: 8px 12px; border: 1px solid #EBE8BF; border-radius: 3px; From 5b0181c925dd327338d256f3ef6d4776e936fbc1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:31:35 -0500 Subject: [PATCH 149/329] Status moved to the right --- .../xmodule/css/combinedopenended/display.scss | 13 +++++++++++++ lms/templates/combined_open_ended.html | 15 +++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index faa3c79419..7e09c85734 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -20,6 +20,19 @@ h2 { color: darken($error-red, 10%); } +section.combined-open-ended { + .status-container + { + float:right; + width:30%; + } + .item-container + { + float:left; + width: 63%; + } +} + section.open-ended-child, section.combined-open-ended-status { @media print { display: block; diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 6b30c3f418..bc68cbe29e 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,12 +1,15 @@
-
${status | n}
+
${status | n}
- % for item in items: -
${item['content'] | n}
- % endfor +
+ % for item in items: +
${item['content'] | n}
+ % endfor - - + + + +
From 97885354162cade069d719490daaf9c61df1052c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:35:33 -0500 Subject: [PATCH 150/329] Get rid of open ended message --- .../xmodule/xmodule/css/combinedopenended/display.scss | 10 ++++++++++ lms/templates/open_ended.html | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 7e09c85734..c1112d72b3 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -21,6 +21,7 @@ h2 { } section.combined-open-ended { + margin-right:20px; .status-container { float:right; @@ -31,6 +32,15 @@ section.combined-open-ended { float:left; width: 63%; } + + &:after + { + content:"."; + display:block; + height:0; + visibility: hidden; + clear:both; + } } section.open-ended-child, section.combined-open-ended-status { diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index 9ade05d0e2..b0ef0be51a 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -26,7 +26,6 @@
-
From f70434970eda90389d96634f6c0837da8badcc6e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:39:50 -0500 Subject: [PATCH 151/329] Make everything look a bit nicer --- lms/templates/combined_open_ended.html | 6 +++++- lms/templates/self_assessment_prompt.html | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index bc68cbe29e..5cd5d76e4c 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -1,8 +1,12 @@
-
${status | n}
+
+

Status


+ ${status | n} +
+

Problem


% for item in items:
${item['content'] | n}
% endfor diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 4ef057fa34..2ec83ef2a7 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -10,7 +10,6 @@
-
${initial_rubric}
From b83df64f9a0258f1475f3adb6175e7e3ddff8e98 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 18:53:36 -0500 Subject: [PATCH 152/329] Inline response to feedback --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 3 +++ .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 3 ++- common/lib/xmodule/xmodule/open_ended_module.py | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index b0fb365d73..4537985d33 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -213,8 +213,11 @@ class CombinedOpenEndedModule(XModule): last_response=task.latest_answer() last_score = task.latest_score() last_post_assessment = task.latest_post_assessment() + last_post_feedback="" if task_type=="openended": last_post_assessment = task.latest_post_assessment(short_feedback=True) + last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) + last_post_assessment = last_post_evaluation max_score = task.max_score() state = task.state last_response_dict={'response' : last_response, 'score' : last_score, diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index f6ad7eb22a..100905e0ae 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -30,6 +30,8 @@ class @CombinedOpenEnded @hint_wrapper = @$('.hint-wrapper') @message_wrapper = @$('.message-wrapper') @submit_button = @$('.submit-button') + @submit_evaluation_button = @$('.submit-evaluation-button') + @submit_evaluation_button.click @message_post @child_state = @el.data('state') @child_type = @el.data('child-type') if @child_type=="openended" @@ -134,7 +136,6 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - save_hint: (event) => event.preventDefault() if @child_state == 'post_assessment' diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index b1931f81be..32245e710b 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -404,6 +404,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): correct = (score_ratio >= 0.66) return correct + def format_feedback_with_evaluation(self,feedback): + context={'msg' : feedback, id : "1", rows : 30, cols : 30} + html= render_to_string('open_ended_evaluation.html', context) + return html + def handle_ajax(self, dispatch, get, system): ''' This is called by courseware.module_render, to handle an AJAX call. From a3c1f7c9f4bb7dd088edc935a673cf8ce61159c6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 19:00:24 -0500 Subject: [PATCH 153/329] Feedback submission moved --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 6 +++--- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- lms/templates/open_ended_evaluation.html | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 100905e0ae..aeaa765b4e 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -20,6 +20,8 @@ class @CombinedOpenEnded @next_problem_button.click @next_problem # valid states: 'initial', 'assessing', 'post_assessment', 'done' Collapsible.setCollapsibles(@el) + @submit_evaluation_button = $('.submit-evaluation-button') + @submit_evaluation_button.click @message_post # Where to put the rubric once we load it @el = $(element).find('section.open-ended-child') @@ -30,8 +32,6 @@ class @CombinedOpenEnded @hint_wrapper = @$('.hint-wrapper') @message_wrapper = @$('.message-wrapper') @submit_button = @$('.submit-button') - @submit_evaluation_button = @$('.submit-evaluation-button') - @submit_evaluation_button.click @message_post @child_state = @el.data('state') @child_type = @el.data('child-type') if @child_type=="openended" @@ -207,7 +207,7 @@ class @CombinedOpenEnded Logger.log 'message_post', @answers fd = new FormData() - feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value + feedback = $('section.evaluation textarea.feedback-on-feedback')[0].value submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 32245e710b..46b6f7e838 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -405,7 +405,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return correct def format_feedback_with_evaluation(self,feedback): - context={'msg' : feedback, id : "1", rows : 30, cols : 30} + context={'msg' : feedback, 'id' : "1", 'rows' : 30, 'cols' : 30} html= render_to_string('open_ended_evaluation.html', context) return html diff --git a/lms/templates/open_ended_evaluation.html b/lms/templates/open_ended_evaluation.html index 018486a1d6..da3f38b6a9 100644 --- a/lms/templates/open_ended_evaluation.html +++ b/lms/templates/open_ended_evaluation.html @@ -17,6 +17,7 @@

Additional comments:

+
\ No newline at end of file From 25d25c004b2468d3f89a02a87a7083478bb38a73 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 19:16:28 -0500 Subject: [PATCH 154/329] Restyling on feedback response --- .../css/combinedopenended/display.scss | 258 +++++++++--------- 1 file changed, 130 insertions(+), 128 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index c1112d72b3..daed84054e 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -44,6 +44,136 @@ section.combined-open-ended { } section.open-ended-child, section.combined-open-ended-status { + + .evaluation { + p { + margin-bottom: 1px; + } + } + + .feedback-on-feedback { + height: 100px; + width: 150px; + margin-right: 0px; + } + + .evaluation-response { + header { + text-align: right; + a { + font-size: .7em; + } + } + } + .evaluation-scoring { + .scoring-list { + list-style-type: none; + margin-left: 3px; + + li { + &:first-child { + margin-left: 0px; + } + display:block; + margin-left: 0px; + + label { + font-size: .9em; + } + } + } + } + .submit-message-container { + margin: 10px 0px ; + } + + .external-grader-message { + section { + padding-left: 20px; + background-color: #FAFAFA; + color: #2C2C2C; + font-family: monospace; + font-size: 1em; + padding-top: 10px; + header { + font-size: 1.4em; + } + + .shortform { + font-weight: bold; + } + + .longform { + padding: 0px; + margin: 0px; + + .result-errors { + margin: 5px; + padding: 10px 10px 10px 40px; + background: url('../images/incorrect-icon.png') center left no-repeat; + li { + color: #B00; + } + } + + .result-output { + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + h4 { + font-family: monospace; + font-size: 1em; + } + + dl { + margin: 0px; + } + + dt { + margin-top: 20px; + } + + dd { + margin-left: 24pt; + } + } + + .result-correct { + background: url('../images/correct-icon.png') left 20px no-repeat; + .result-actual-output { + color: #090; + } + } + + .result-incorrect { + background: url('../images/incorrect-icon.png') left 20px no-repeat; + .result-actual-output { + color: #B00; + } + } + + .markup-text{ + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + bs { + color: #BB0000; + } + + bg { + color: #BDA046; + } + } + } + } + } +} + +section.open-ended-child { @media print { display: block; width: auto; @@ -265,50 +395,6 @@ section.open-ended-child, section.combined-open-ended-status { } } - .evaluation { - p { - margin-bottom: 4px; - } - } - - - .feedback-on-feedback { - height: 100px; - margin-right: 20px; - } - - .evaluation-response { - header { - text-align: right; - a { - font-size: .85em; - } - } - } - - .evaluation-scoring { - .scoring-list { - list-style-type: none; - margin-left: 3px; - - li { - &:first-child { - margin-left: 0px; - } - display:inline; - margin-left: 50px; - - label { - font-size: .9em; - } - - } - } - - } - .submit-message-container { - margin: 10px 0px ; - } form.option-input { margin: -10px 0 20px; @@ -444,88 +530,4 @@ section.open-ended-child, section.combined-open-ended-status { font-size: 0.9em; } - .external-grader-message { - section { - padding-left: 20px; - background-color: #FAFAFA; - color: #2C2C2C; - font-family: monospace; - font-size: 1em; - padding-top: 10px; - header { - font-size: 1.4em; - } - - .shortform { - font-weight: bold; - } - - .longform { - padding: 0px; - margin: 0px; - - .result-errors { - margin: 5px; - padding: 10px 10px 10px 40px; - background: url('../images/incorrect-icon.png') center left no-repeat; - li { - color: #B00; - } - } - - .result-output { - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; - - h4 { - font-family: monospace; - font-size: 1em; - } - - dl { - margin: 0px; - } - - dt { - margin-top: 20px; - } - - dd { - margin-left: 24pt; - } - } - - .result-correct { - background: url('../images/correct-icon.png') left 20px no-repeat; - .result-actual-output { - color: #090; - } - } - - .result-incorrect { - background: url('../images/incorrect-icon.png') left 20px no-repeat; - .result-actual-output { - color: #B00; - } - } - - .markup-text{ - margin: 5px; - padding: 20px 0px 15px 50px; - border-top: 1px solid #DDD; - border-left: 20px solid #FAFAFA; - - bs { - color: #BB0000; - } - - bg { - color: #BDA046; - } - } - } - } - } } From 349a43f31c6d1e78953aadb9fc5a6fc7228a6209 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 7 Jan 2013 19:19:51 -0500 Subject: [PATCH 155/329] Decouple message post from state logic --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index aeaa765b4e..2c8cbcda4b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -220,7 +220,6 @@ class @CombinedOpenEnded else fd.append('score', score) - settings = type: "POST" data: fd @@ -228,11 +227,8 @@ class @CombinedOpenEnded contentType: false success: (response) => @gentle_alert response.msg - @$('section.evaluation').slideToggle() + $('section.evaluation').slideToggle() @message_wrapper.html(response.message_html) - @child_state = 'done' - @allow_reset = response.allow_reset - @rebind() $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) From 5273d44788ce59552324a2ed2ded3f6743cacb76 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 10:24:36 -0500 Subject: [PATCH 156/329] Fix css and display current step --- .../css/combinedopenended/display.scss | 18 ++++++++++- lms/templates/combined_open_ended_status.html | 32 +++++++++++-------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index daed84054e..e15a34453a 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -21,7 +21,6 @@ h2 { } section.combined-open-ended { - margin-right:20px; .status-container { float:right; @@ -45,6 +44,23 @@ section.combined-open-ended { section.open-ended-child, section.combined-open-ended-status { + .statusitem { + background-color: #FAFAFA; + color: #2C2C2C; + font-family: monospace; + font-size: 1em; + padding-top: 10px; + } + + .statusitem-current { + background-color: #BEBEBE; + color: #2C2C2C; + font-family: monospace; + font-size: 1em; + padding-top: 10px; + } + + .evaluation { p { margin-bottom: 1px; diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 7bc66d954f..c8a0177edd 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -1,17 +1,23 @@
- %for status in status_list: -
- Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} - %if status['type']=="openended": -
-
- Show feedback from step ${status['task_number']} -
-
- ${status['post_assessment']} -
-
+ %for i in xrange(0,len(status_list)): + <%status=status_list[i]%> + %if i==len(status_list)-1: +
+ %else: +
%endif -
+ + Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} + %if status['type']=="openended": +
+
+ Show feedback from step ${status['task_number']} +
+
+ ${status['post_assessment']} +
+
+ %endif +
%endfor
\ No newline at end of file From 24bfe44049e79cd29f3735ad2faf03cce21c20dc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 10:57:47 -0500 Subject: [PATCH 157/329] Some restyling, automatically check for feedback, remove reload button --- .../css/combinedopenended/display.scss | 8 +++---- .../js/src/combinedopenended/display.coffee | 21 ++++++++++++++----- .../lib/xmodule/xmodule/open_ended_module.py | 7 ++++++- lms/templates/open_ended.html | 1 - 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index e15a34453a..5ac7066c6d 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -24,12 +24,12 @@ section.combined-open-ended { .status-container { float:right; - width:30%; + width:40%; } .item-container { float:left; - width: 63%; + width: 53%; } &:after @@ -68,8 +68,8 @@ section.open-ended-child, section.combined-open-ended-status { } .feedback-on-feedback { - height: 100px; - width: 150px; + height: 150px; + width: 250px; margin-right: 0px; } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 2c8cbcda4b..e868186bde 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -35,7 +35,6 @@ class @CombinedOpenEnded @child_state = @el.data('state') @child_type = @el.data('child-type') if @child_type=="openended" - @reload_button = @$('.reload-button') @skip_button = @$('.skip-button') @skip_button.click @skip_post_assessment @@ -58,7 +57,6 @@ class @CombinedOpenEnded @next_problem_button.hide() @hint_area.attr('disabled', false) if @child_type=="openended" - @reload_button.hide() @skip_button.hide() if @child_state == 'initial' @answer_area.attr("disabled", false) @@ -70,10 +68,9 @@ class @CombinedOpenEnded @submit_button.click @save_assessment if @child_type == "openended" @submit_button.hide() - @reload_button.show() + @queueing() else if @child_state == 'post_assessment' if @child_type=="openended" - @reload_button.hide() @skip_button.show() @skip_post_assessment() @answer_area.attr("disabled", true) @@ -237,4 +234,18 @@ class @CombinedOpenEnded @el.find('.open-ended-alert').remove() alert_elem = "
" + msg + "
" @el.find('.open-ended-action').after(alert_elem) - @el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) \ No newline at end of file + @el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700) + + queueing: => + if @child_state=="assessing" and @child_type=="openended" + if window.queuePollerID # Only one poller 'thread' per Problem + window.clearTimeout(window.queuePollerID) + window.queuePollerID = window.setTimeout(@poll, 10000) + + poll: => + $.postWithPrefix "#{@ajax_url}/check_for_score", (response) => + if response.state == "done" or response.state=="post_assessment" + delete window.queuePollerID + location.reload() + else + window.queuePollerID = window.setTimeout(@poll, 10000) \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 46b6f7e838..ba1c68511f 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -405,7 +405,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return correct def format_feedback_with_evaluation(self,feedback): - context={'msg' : feedback, 'id' : "1", 'rows' : 30, 'cols' : 30} + context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} html= render_to_string('open_ended_evaluation.html', context) return html @@ -424,6 +424,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'score_update': self.update_score, 'save_post_assessment' : self.message_post, 'skip_post_assessment' : self.skip_post_assessment, + 'check_for_score' : self.check_for_score, } if dispatch not in handlers: @@ -438,6 +439,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) + def check_for_score(self, get, system): + state = self.state + return {'state' : state} + def save_answer(self, get, system): if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index b0ef0be51a..c1c7288d9e 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -29,7 +29,6 @@ -
${msg|n}
From 3620fd93d9d13fb0ca66f69dfdf1a7b46aff38d2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 12:13:39 -0500 Subject: [PATCH 158/329] Create a result container and put results into it --- .../xmodule/combined_open_ended_module.py | 9 +++++++++ .../xmodule/css/combinedopenended/display.scss | 7 +++++++ .../js/src/combinedopenended/display.coffee | 17 +++++++++++++++++ lms/templates/combined_open_ended.html | 4 ++++ lms/templates/combined_open_ended_results.html | 3 +++ lms/templates/combined_open_ended_status.html | 9 ++------- lms/templates/open_ended_evaluation.html | 2 +- 7 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 lms/templates/combined_open_ended_results.html diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4537985d33..c6ed8b2ed1 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -248,6 +248,14 @@ class CombinedOpenEndedModule(XModule): pass return return_html + def get_results(self, get): + task_number=get['task_number'] + self.update_task_states() + response_dict=self.get_last_response(task_number) + context = {'results' : response_dict['post_assessment']} + html = render_to_string('combined_open_ended_results.html', context) + return html + def handle_ajax(self, dispatch, get): """ This is called by courseware.module_render, to handle an AJAX call. @@ -262,6 +270,7 @@ class CombinedOpenEndedModule(XModule): handlers = { 'next_problem': self.next_problem, 'reset': self.reset, + 'get_results' : self.get_results } if dispatch not in handlers: diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 5ac7066c6d..d8e36361a6 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -32,6 +32,13 @@ section.combined-open-ended { width: 53%; } + .result-container + { + float:left; + width: 93%; + position:relative; + } + &:after { content:"."; diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index e868186bde..922f4082b0 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -18,11 +18,17 @@ class @CombinedOpenEnded @reset_button.click @reset @next_problem_button = @$('.next-step-button') @next_problem_button.click @next_problem + + @show_results_button=@$('.show-results-button') + @show_results_button.click @show_results + # valid states: 'initial', 'assessing', 'post_assessment', 'done' Collapsible.setCollapsibles(@el) @submit_evaluation_button = $('.submit-evaluation-button') @submit_evaluation_button.click @message_post + @results_container = $('.result-container') + # Where to put the rubric once we load it @el = $(element).find('section.open-ended-child') @errors_area = @$('.error') @@ -49,6 +55,17 @@ class @CombinedOpenEnded $: (selector) -> $(selector, @el) + show_results: (event) => + status_item = $(event.target).parent().parent() + status_number = status_item.data('status-number') + data = {'task_number' : status_number} + $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => + if response.success + @results_container.after(response.html).remove() + @Collapsible.setCollapsibles(@results_container) + else + @errors_area.html(response.error) + rebind: () => # rebind to the appropriate function for the current state @submit_button.unbind('click') diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 5cd5d76e4c..4c0aaa1042 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -15,5 +15,9 @@ + +
+

Results


+
diff --git a/lms/templates/combined_open_ended_results.html b/lms/templates/combined_open_ended_results.html new file mode 100644 index 0000000000..12af086cfe --- /dev/null +++ b/lms/templates/combined_open_ended_results.html @@ -0,0 +1,3 @@ +
+ ${results | n} +
\ No newline at end of file diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index c8a0177edd..6109df2f26 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -9,13 +9,8 @@ Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} %if status['type']=="openended": - diff --git a/lms/templates/open_ended_evaluation.html b/lms/templates/open_ended_evaluation.html index da3f38b6a9..71ce6d5056 100644 --- a/lms/templates/open_ended_evaluation.html +++ b/lms/templates/open_ended_evaluation.html @@ -20,4 +20,4 @@ - \ No newline at end of file +incorrect-icon.png \ No newline at end of file From 943c0ff8207e3cb065e871128c9a8399179ab9df Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:01:35 -0500 Subject: [PATCH 159/329] Display results separately from submission logic --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 6 +++--- .../xmodule/css/combinedopenended/display.scss | 11 +++++++---- .../xmodule/js/src/combinedopenended/display.coffee | 1 + lms/templates/combined_open_ended_results.html | 1 + lms/templates/open_ended.html | 4 ---- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index c6ed8b2ed1..aec330cb49 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -215,7 +215,7 @@ class CombinedOpenEndedModule(XModule): last_post_assessment = task.latest_post_assessment() last_post_feedback="" if task_type=="openended": - last_post_assessment = task.latest_post_assessment(short_feedback=True) + last_post_assessment = task.latest_post_assessment(short_feedback=False) last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation max_score = task.max_score() @@ -249,12 +249,12 @@ class CombinedOpenEndedModule(XModule): return return_html def get_results(self, get): - task_number=get['task_number'] + task_number=int(get['task_number']) self.update_task_states() response_dict=self.get_last_response(task_number) context = {'results' : response_dict['post_assessment']} html = render_to_string('combined_open_ended_results.html', context) - return html + return {'html' : html, 'success' : True} def handle_ajax(self, dispatch, get): """ diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index d8e36361a6..6adb31aa5d 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -30,6 +30,7 @@ section.combined-open-ended { { float:left; width: 53%; + padding-bottom: 20px; } .result-container @@ -49,7 +50,7 @@ section.combined-open-ended { } } -section.open-ended-child, section.combined-open-ended-status { +section.combined-open-ended-status { .statusitem { background-color: #FAFAFA; @@ -66,9 +67,11 @@ section.open-ended-child, section.combined-open-ended-status { font-size: 1em; padding-top: 10px; } +} +div.result-container { - .evaluation { + .evaluation { p { margin-bottom: 1px; } @@ -84,7 +87,7 @@ section.open-ended-child, section.combined-open-ended-status { header { text-align: right; a { - font-size: .7em; + font-size: .85em; } } } @@ -97,7 +100,7 @@ section.open-ended-child, section.combined-open-ended-status { &:first-child { margin-left: 0px; } - display:block; + display:inline; margin-left: 0px; label { diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 922f4082b0..1dbe3fe9ea 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -62,6 +62,7 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => if response.success @results_container.after(response.html).remove() + @results_container = $('.result-container') @Collapsible.setCollapsibles(@results_container) else @errors_area.html(response.error) diff --git a/lms/templates/combined_open_ended_results.html b/lms/templates/combined_open_ended_results.html index 12af086cfe..75c5596e4b 100644 --- a/lms/templates/combined_open_ended_results.html +++ b/lms/templates/combined_open_ended_results.html @@ -1,3 +1,4 @@
+

Results


${results | n}
\ No newline at end of file diff --git a/lms/templates/open_ended.html b/lms/templates/open_ended.html index c1c7288d9e..cda3282a45 100644 --- a/lms/templates/open_ended.html +++ b/lms/templates/open_ended.html @@ -28,8 +28,4 @@
- -
- ${msg|n} -
From 7b08114805a3410c4633817db38943469887e244 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:09:48 -0500 Subject: [PATCH 160/329] More restyling, fix buttons --- .../lib/xmodule/xmodule/css/combinedopenended/display.scss | 5 ++--- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 6adb31aa5d..7f52cd9623 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -30,7 +30,7 @@ section.combined-open-ended { { float:left; width: 53%; - padding-bottom: 20px; + padding-bottom: 50px; } .result-container @@ -78,8 +78,7 @@ div.result-container { } .feedback-on-feedback { - height: 150px; - width: 250px; + height: 100px; margin-right: 0px; } diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 1dbe3fe9ea..0e285dce25 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -62,8 +62,10 @@ class @CombinedOpenEnded $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => if response.success @results_container.after(response.html).remove() - @results_container = $('.result-container') - @Collapsible.setCollapsibles(@results_container) + @results_container = $('div.result-container') + @submit_evaluation_button = $('.submit-evaluation-button') + @submit_evaluation_button.click @message_post + Collapsible.setCollapsibles(@results_container) else @errors_area.html(response.error) From af705ca1af28445abb85ca1a5268100654970ac7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:12:21 -0500 Subject: [PATCH 161/329] Remove html --- lms/templates/open_ended_evaluation.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/open_ended_evaluation.html b/lms/templates/open_ended_evaluation.html index 71ce6d5056..da3f38b6a9 100644 --- a/lms/templates/open_ended_evaluation.html +++ b/lms/templates/open_ended_evaluation.html @@ -20,4 +20,4 @@ -incorrect-icon.png \ No newline at end of file + \ No newline at end of file From bccafe6c3195403ce321efc4fb20e56d1cfc18e8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:23:54 -0500 Subject: [PATCH 162/329] Add in correctness display --- .../xmodule/combined_open_ended_module.py | 16 ++++++++++++---- common/lib/xmodule/xmodule/open_ended_module.py | 7 ------- common/lib/xmodule/xmodule/openendedchild.py | 11 +++++++++++ lms/templates/combined_open_ended.html | 1 - lms/templates/combined_open_ended_results.html | 2 +- lms/templates/combined_open_ended_status.html | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index aec330cb49..957b978407 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -218,11 +218,19 @@ class CombinedOpenEndedModule(XModule): last_post_assessment = task.latest_post_assessment(short_feedback=False) last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation + last_correctness = task.is_last_response_correct() max_score = task.max_score() state = task.state - last_response_dict={'response' : last_response, 'score' : last_score, - 'post_assessment' : last_post_assessment, - 'type' : task_type, 'max_score' : max_score, 'state' : state, 'human_state' : task.HUMAN_NAMES[state]} + last_response_dict={ + 'response' : last_response, + 'score' : last_score, + 'post_assessment' : last_post_assessment, + 'type' : task_type, + 'max_score' : max_score, + 'state' : state, + 'human_state' : task.HUMAN_NAMES[state], + 'correct' : last_correctness + } return last_response_dict @@ -252,7 +260,7 @@ class CombinedOpenEndedModule(XModule): task_number=int(get['task_number']) self.update_task_states() response_dict=self.get_last_response(task_number) - context = {'results' : response_dict['post_assessment']} + context = {'results' : response_dict['post_assessment'], 'task_number' : task_number+1} html = render_to_string('combined_open_ended_results.html', context) return {'html' : html, 'success' : True} diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ba1c68511f..9cdbdd54e2 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -397,13 +397,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' - def is_submission_correct(self, score): - correct=False - if(isinstance(score,(int, long, float, complex))): - score_ratio = int(score) / float(self.max_score()) - correct = (score_ratio >= 0.66) - return correct - def format_feedback_with_evaluation(self,feedback): context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} html= render_to_string('open_ended_evaluation.html', context) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 236bd03c4c..73bd8f3957 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -250,5 +250,16 @@ class OpenEndedChild(): def handle_ajax(self): pass + def is_submission_correct(self, score): + correct=False + if(isinstance(score,(int, long, float, complex))): + score_ratio = int(score) / float(self.max_score()) + correct = (score_ratio >= 0.66) + return correct + + def is_last_response_correct(self): + score=self.get_score() + return self.is_submission_correct(score) + diff --git a/lms/templates/combined_open_ended.html b/lms/templates/combined_open_ended.html index 4c0aaa1042..71c22085e3 100644 --- a/lms/templates/combined_open_ended.html +++ b/lms/templates/combined_open_ended.html @@ -17,7 +17,6 @@
-

Results


diff --git a/lms/templates/combined_open_ended_results.html b/lms/templates/combined_open_ended_results.html index 75c5596e4b..db86e95016 100644 --- a/lms/templates/combined_open_ended_results.html +++ b/lms/templates/combined_open_ended_results.html @@ -1,4 +1,4 @@
-

Results


+

Results from Step ${task_number}


${results | n}
\ No newline at end of file diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 6109df2f26..9d51bd67fd 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,7 +8,7 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} - %if status['type']=="openended": + %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']: From 5150d6cdeec2664773acb7ac67d3f20461a120aa Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:28:02 -0500 Subject: [PATCH 163/329] Add in better status indicators --- common/lib/xmodule/xmodule/openendedchild.py | 3 ++- lms/templates/combined_open_ended_status.html | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 73bd8f3957..5fccdee4f6 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -259,7 +259,8 @@ class OpenEndedChild(): def is_last_response_correct(self): score=self.get_score() - return self.is_submission_correct(score) + correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' + return diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index 9d51bd67fd..a653a84d13 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,6 +8,15 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} + % if state == 'initial': + Unanswered + % elif state in ['done', 'post_assessment'] and correct == 'correct': + Correct + % elif state in ['done', 'post_assessment'] and correct == 'incorrect': + Incorrect + % elif state == 'assessing': + Submitted for grading + % endif %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
Show results from step ${status['task_number']} From 3bf532e199e62a942ccbb2f3a6164bbb2f142015 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:45:07 -0500 Subject: [PATCH 164/329] Check marks in each step --- .../css/combinedopenended/display.scss | 29 +++++++++++++++++++ common/lib/xmodule/xmodule/openendedchild.py | 4 +-- lms/templates/combined_open_ended_status.html | 17 ++++++----- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 7f52cd9623..be86757aee 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -67,6 +67,35 @@ section.combined-open-ended-status { font-size: 1em; padding-top: 10px; } + + span { + &.unanswered { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + position: relative; + width: 14px; + float: right; + } + + &.correct { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + position: relative; + width: 25px; + float: right; + } + + &.incorrect { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + position: relative; + float: right; + } + } } div.result-container { diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5fccdee4f6..5d69323e4a 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -258,9 +258,9 @@ class OpenEndedChild(): return correct def is_last_response_correct(self): - score=self.get_score() + score=self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' - return + return correctness diff --git a/lms/templates/combined_open_ended_status.html b/lms/templates/combined_open_ended_status.html index a653a84d13..34a5dd0d79 100644 --- a/lms/templates/combined_open_ended_status.html +++ b/lms/templates/combined_open_ended_status.html @@ -8,15 +8,16 @@ %endif Step ${status['task_number']} (${status['human_state']}) : ${status['score']} / ${status['max_score']} - % if state == 'initial': - Unanswered - % elif state in ['done', 'post_assessment'] and correct == 'correct': - Correct - % elif state in ['done', 'post_assessment'] and correct == 'incorrect': - Incorrect - % elif state == 'assessing': - Submitted for grading + % if status['state'] == 'initial': + + % elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'correct': + + % elif status['state'] in ['done', 'post_assessment'] and status['correct'] == 'incorrect': + + % elif status['state'] == 'assessing': + % endif + %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
Show results from step ${status['task_number']} From 9e14e22c91e28376424b2fb41ac642a82d3642f3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 15:53:36 -0500 Subject: [PATCH 165/329] Move prompt and rubric to combined open ended instead of defining them in each task --- .../xmodule/combined_open_ended_module.py | 12 +++++++++--- common/lib/xmodule/xmodule/open_ended_module.py | 17 ++++++----------- common/lib/xmodule/xmodule/openendedchild.py | 3 +++ .../xmodule/xmodule/self_assessment_module.py | 2 -- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 957b978407..4788c43382 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -98,6 +98,8 @@ class CombinedOpenEndedModule(XModule): self.static_data = { 'max_score' : self._max_score, 'max_attempts' : self.max_attempts, + 'prompt' : definition['prompt'], + 'rubric' : definition['rubric'] } self.task_xml=definition['task_xml'] @@ -371,16 +373,20 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): 'hintprompt': 'some-html' } """ - expected_children = ['task'] + expected_children = ['task', 'rubric', 'prompt'] for child in expected_children: if len(xml_object.xpath(child)) == 0 : raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) - def parse(k): + def parse_task(k): """Assumes that xml_object has child k""" return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] - return {'task_xml': parse('task')} + def parse(k): + """Assumes that xml_object has child k""" + return xml_object.xpath(k)[0] + + return {'task_xml': parse_task('task'), 'prompt' : parse('prompt'), 'rubric' : parse('rubric')} def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 9cdbdd54e2..b795db8228 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -40,8 +40,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): oeparam = definition['oeparam'] - prompt = definition['prompt'] - rubric = definition['rubric'] self.url = definition.get('url', None) self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE) @@ -53,12 +51,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if oeparam is None: raise ValueError("No oeparam found in problem xml.") - if prompt is None: + if self.prompt is None: raise ValueError("No prompt found in problem xml.") - if rubric is None: + if self.rubric is None: raise ValueError("No rubric found in problem xml.") - self._parse(oeparam, prompt, rubric, system) + self._parse(oeparam, self.prompt, self.rubric, system) if self.created=="True" and self.state == self.ASSESSING: self.created="False" @@ -530,7 +528,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): } """ - for child in ['openendedrubric', 'prompt', 'openendedparam']: + for child in ['openendedparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) @@ -538,10 +536,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'rubric': parse('openendedrubric'), - 'prompt': parse('prompt'), - 'oeparam': parse('openendedparam'), - } + return {'oeparam': parse('openendedparam'),} def definition_to_xml(self, resource_fs): @@ -553,7 +548,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in ['openendedrubric', 'prompt', 'openendedparam']: + for child in ['openendedparam']: add_child(child) return elt diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5d69323e4a..4a81703919 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -127,6 +127,9 @@ class OpenEndedChild(): self.attempts = instance_state.get('attempts', 0) self.max_attempts = static_data['max_attempts'] + self.prompt = static_data['prompt'] + self.rubric = static_data['rubric'] + # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = static_data['max_score'] diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 88c47f92ef..7050ff991b 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -33,8 +33,6 @@ log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): - self.rubric = definition['rubric'] - self.prompt = definition['prompt'] self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] From 3aa0daaa764176823b9d9e2811f73d7b4bfcdf05 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:00:30 -0500 Subject: [PATCH 166/329] Add in proper prompt and rubric parsing --- common/lib/xmodule/xmodule/open_ended_module.py | 1 + common/lib/xmodule/xmodule/self_assessment_module.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index b795db8228..3047c86888 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -76,6 +76,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): prompt_string = stringify_children(prompt) rubric_string = stringify_children(rubric) self.prompt=prompt_string + self.rubric=rubric_string grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 7050ff991b..db082a8b4f 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -35,6 +35,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] + self.prompt = stringify_children(prompt) + self.rubric = stringify_children(rubric) def get_html(self, system): #set context variables and render template @@ -270,7 +272,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): 'hintprompt': 'some-html' } """ - expected_children = ['rubric', 'prompt', 'submitmessage', 'hintprompt'] + expected_children = ['submitmessage', 'hintprompt'] for child in expected_children: if len(xml_object.xpath(child)) != 1: raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child)) @@ -279,9 +281,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return stringify_children(xml_object.xpath(k)[0]) - return {'rubric': parse('rubric'), - 'prompt': parse('prompt'), - 'submitmessage': parse('submitmessage'), + return {'submitmessage': parse('submitmessage'), 'hintprompt': parse('hintprompt'), } @@ -294,7 +294,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): child_node = etree.fromstring(child_str) elt.append(child_node) - for child in ['rubric', 'prompt', 'submitmessage', 'hintprompt']: + for child in ['submitmessage', 'hintprompt']: add_child(child) return elt From dcb33f1d5f2d2189968e813764ba4f97acca09ee Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:04:09 -0500 Subject: [PATCH 167/329] Fix prompt and rubric passing --- common/lib/xmodule/xmodule/self_assessment_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index db082a8b4f..52701a8cf1 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -35,8 +35,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): def setup_response(self, system, location, definition, descriptor): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] - self.prompt = stringify_children(prompt) - self.rubric = stringify_children(rubric) + self.prompt = stringify_children(self.prompt) + self.rubric = stringify_children(self.rubric) def get_html(self, system): #set context variables and render template From b6a49f33ad0d678265bff5a826b6e2b678711e31 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:31:33 -0500 Subject: [PATCH 168/329] parse out min and max score to advance --- .../lib/xmodule/xmodule/combined_open_ended_module.py | 11 +++++++---- common/lib/xmodule/xmodule/open_ended_module.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4788c43382..0e9874ec4f 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -82,7 +82,6 @@ class CombinedOpenEndedModule(XModule): # element. # Scores are on scale from 0 to max_score system.set('location', location) - log.debug(system.location) self.current_task_number = instance_state.get('current_task_number', 0) self.task_states= instance_state.get('task_states', []) @@ -147,7 +146,13 @@ class CombinedOpenEndedModule(XModule): children=self.child_modules() self.current_task_descriptor=children['descriptors'][current_task_type](self.system) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree.fromstring(self.current_task_xml),self.system) + etree_xml=etree.fromstring(self.current_task_xml) + min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) + max_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',self._max_score)) + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + + self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) @@ -165,8 +170,6 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.overwrite_state(current_task_state) self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) - log.debug(self.current_task.get_instance_state()) - log.debug(self.get_instance_state()) return True def get_context(self): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3047c86888..0e16156f1a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -528,7 +528,6 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): 'oeparam': 'some-html' } """ - for child in ['openendedparam']: if len(xml_object.xpath(child)) != 1: raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) From 1ae94ce6366f2dc00a7159a9e06d4578c262bf8d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 16:45:21 -0500 Subject: [PATCH 169/329] Hopefully allow for submission to be reset early --- .../xmodule/combined_open_ended_module.py | 33 +++++++++++++++---- .../js/src/combinedopenended/display.coffee | 11 ++++--- common/lib/xmodule/xmodule/openendedchild.py | 2 +- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 0e9874ec4f..d31da49978 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -147,10 +147,11 @@ class CombinedOpenEndedModule(XModule): self.current_task_descriptor=children['descriptors'][current_task_type](self.system) etree_xml=etree.fromstring(self.current_task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',self._max_score)) + if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) + allow_reset=self.check_allow_reset() + if allow_reset: + return False self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: @@ -172,6 +173,17 @@ class CombinedOpenEndedModule(XModule): return True + def check_allow_reset(self): + allow_reset=False + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + current_response_data=self.get_last_response(self.current_task_number) + + if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt'] if response.success @child_state = 'done' - @allow_reset = response.allow_reset @rebind() else @errors_area.html(response.error) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 4a81703919..304271c620 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -200,7 +200,7 @@ class OpenEndedChild(): def _allow_reset(self): """Can the module be reset?""" - return self.state == self.DONE and self.attempts < self.max_attempts + return (self.state == self.DONE and self.attempts < self.max_attempts) def max_score(self): """ From b13f94798fd3ff5fe37c5c509a9b94c78de671f1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 17:02:56 -0500 Subject: [PATCH 170/329] Add in allow reset action --- .../xmodule/combined_open_ended_module.py | 39 ++++++++++++------- .../js/src/combinedopenended/display.coffee | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index d31da49978..f9c610d51a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -88,6 +88,7 @@ class CombinedOpenEndedModule(XModule): self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) + self.allow_reset = instance_state.get('ready_to_reset', False) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) # Used for progress / grading. Currently get credit just for @@ -149,8 +150,8 @@ class CombinedOpenEndedModule(XModule): etree_xml=etree.fromstring(self.current_task_xml) if self.current_task_number>0: - allow_reset=self.check_allow_reset() - if allow_reset: + self.allow_reset=self.check_allow_reset() + if self.allow_reset: return False self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) @@ -174,15 +175,15 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): - allow_reset=False if self.current_task_number>0: last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_last_response(self.current_task_number) + current_response_data=self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt']last_response_data['score'] or current_response_data['max_score_to_attempt']=(len(self.task_xml)): self.state=self.DONE @@ -334,6 +343,7 @@ class CombinedOpenEndedModule(XModule): 'error': 'Too many attempts.' } self.state=self.INITIAL + self.allow_reset=False for i in xrange(0,len(self.task_xml)): self.current_task_number=i self.setup_next_task(reset=True) @@ -354,6 +364,7 @@ class CombinedOpenEndedModule(XModule): 'state': self.state, 'task_states': self.task_states, 'attempts': self.attempts, + 'ready_to_reset' : self.allow_reset, } return json.dumps(state) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 0d420a8514..3929ebe78a 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -79,7 +79,7 @@ class @CombinedOpenEnded if @child_type=="openended" @skip_button.hide() - if @allow_reset + if @allow_reset=="True" @reset_button.show() @submit_button.hide() @answer_area.attr("disabled", true) From adcbfbb6035e748cf4b0cecaaf2e4fee3d563113 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:00:46 -0500 Subject: [PATCH 171/329] Fix reset --- .../xmodule/combined_open_ended_module.py | 55 ++++++++++--------- .../js/src/combinedopenended/display.coffee | 3 +- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f9c610d51a..3c756f6834 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -142,6 +142,12 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.task_states[self.current_task_number] self.current_task_xml=self.task_xml[self.current_task_number] + + if self.current_task_number>0: + self.allow_reset=self.check_allow_reset() + if self.allow_reset: + self.current_task_number=self.current_task_number-1 + current_task_type=self.get_tag_name(self.current_task_xml) children=self.child_modules() @@ -149,11 +155,6 @@ class CombinedOpenEndedModule(XModule): self.current_task_descriptor=children['descriptors'][current_task_type](self.system) etree_xml=etree.fromstring(self.current_task_xml) - if self.current_task_number>0: - self.allow_reset=self.check_allow_reset() - if self.allow_reset: - return False - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) if current_task_state is None and self.current_task_number==0: self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) @@ -175,13 +176,14 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): - if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_current_attributes(self.current_task_number) + if not self.allow_reset: + if self.current_task_number>0: + last_response_data=self.get_last_response(self.current_task_number-1) + current_response_data=self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt']last_response_data['score'] or current_response_data['max_score_to_attempt']=(len(self.task_xml)): - self.state=self.DONE - self.current_task_number=len(self.task_xml)-1 - else: - self.state=self.INITIAL - changed=True - self.setup_next_task() + if not self.allow_reset: + self.task_states[self.current_task_number] = self.current_task.get_instance_state() + current_task_state=json.loads(self.task_states[self.current_task_number]) + if current_task_state['state']==self.DONE: + self.current_task_number+=1 + if self.current_task_number>=(len(self.task_xml)): + self.state=self.DONE + self.current_task_number=len(self.task_xml)-1 + else: + self.state=self.INITIAL + changed=True + self.setup_next_task() return changed def update_task_states_ajax(self,return_html): @@ -335,7 +336,8 @@ class CombinedOpenEndedModule(XModule): (error only present if not success) """ if self.state != self.DONE: - return self.out_of_sync_error(get) + if not self.allow_reset: + return self.out_of_sync_error(get) if self.attempts > self.max_attempts: return { @@ -350,6 +352,7 @@ class CombinedOpenEndedModule(XModule): self.current_task.reset(self.system) self.task_states[self.current_task_number]=self.current_task.get_instance_state() self.current_task_number=0 + self.allow_reset=False self.setup_next_task() return {'success': True, 'html' : self.get_html_nonsystem()} diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 3929ebe78a..a7e01e8a9b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -187,7 +187,7 @@ class @CombinedOpenEnded reset: (event) => event.preventDefault() - if @child_state == 'done' + if @child_state == 'done' or @allow_reset=="True" $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success @answer_area.val('') @@ -196,6 +196,7 @@ class @CombinedOpenEnded @message_wrapper.html('') @child_state = 'initial' @combined_open_ended.after(response.html).remove() + @allow_reset="False" @reinitialize(@element) @rebind() @reset_button.hide() From bc97a507e047cb8fca8626b303e1507fa57ae930 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:05:01 -0500 Subject: [PATCH 172/329] Next step logic --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 3c756f6834..ee36690b1c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -326,7 +326,7 @@ class CombinedOpenEndedModule(XModule): def next_problem(self, get): self.update_task_states() - return {'success' : True, 'html' : self.get_html_nonsystem()} + return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} def reset(self, get): """ diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index a7e01e8a9b..682ba983bd 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -218,7 +218,10 @@ class @CombinedOpenEnded @reinitialize(@element) @rebind() @next_problem_button.hide() - @gentle_alert "Moved to next step." + if response.allow_reset=="False" + @gentle_alert "Moved to next step." + else + @gentle_alert "Your score did not meet the criteria to move to the next step." else @errors_area.html(response.error) else From 38a81b461f413d8a7e4cc00a7793d0de8d8b6894 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:33:41 -0500 Subject: [PATCH 173/329] Remove open ended grading stuff, fix JS variable --- common/lib/capa/capa/inputtypes.py | 48 -- common/lib/capa/capa/responsetypes.py | 433 +----------------- .../xmodule/js/src/capa/display.coffee | 30 -- .../js/src/combinedopenended/display.coffee | 2 +- .../lib/xmodule/xmodule/open_ended_module.py | 2 +- 5 files changed, 3 insertions(+), 512 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index e3eb47acc5..1d3646fefc 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -735,51 +735,3 @@ class ChemicalEquationInput(InputTypeBase): registry.register(ChemicalEquationInput) #----------------------------------------------------------------------------- - -class OpenEndedInput(InputTypeBase): - """ - A text area input for code--uses codemirror, does syntax highlighting, special tab handling, - etc. - """ - - template = "openendedinput.html" - tags = ['openendedinput'] - - # pulled out for testing - submitted_msg = ("Feedback not yet available. Reload to check again. " - "Once the problem is graded, this message will be " - "replaced with the grader's feedback.") - - @classmethod - def get_attributes(cls): - """ - Convert options to a convenient format. - """ - return [Attribute('rows', '30'), - Attribute('cols', '80'), - Attribute('hidden', ''), - ] - - def setup(self): - """ - Implement special logic: handle queueing state, and default input. - """ - # if no student input yet, then use the default input given by the problem - if not self.value: - self.value = self.xml.text - - # Check if problem has been queued - self.queue_len = 0 - # Flag indicating that the problem has been queued, 'msg' is length of queue - if self.status == 'incomplete': - self.status = 'queued' - self.queue_len = self.msg - self.msg = self.submitted_msg - - def _extra_context(self): - """Defined queue_len, add it """ - return {'queue_len': self.queue_len,} - -registry.register(OpenEndedInput) - -#----------------------------------------------------------------------------- diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 1bc34b70a3..3d97cb0bea 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1815,436 +1815,6 @@ class ImageResponse(LoncapaResponse): return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]), dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) #----------------------------------------------------------------------------- - -class OpenEndedResponse(LoncapaResponse): - """ - Grade student open ended responses using an external grading system, - accessed through the xqueue system. - - Expects 'xqueue' dict in ModuleSystem with the following keys that are - needed by OpenEndedResponse: - - system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL - where results are posted (string), - } - - External requests are only submitted for student submission grading - (i.e. and not for getting reference answers) - - By default, uses the OpenEndedResponse.DEFAULT_QUEUE queue. - """ - - DEFAULT_QUEUE = 'open-ended' - DEFAULT_MESSAGE_QUEUE = 'open-ended-message' - response_tag = 'openendedresponse' - allowed_inputfields = ['openendedinput'] - max_inputfields = 1 - - def setup_response(self): - ''' - Configure OpenEndedResponse from XML. - ''' - xml = self.xml - self.url = xml.get('url', None) - self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE) - self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE) - - # The openendedparam tag encapsulates all grader settings - oeparam = self.xml.find('openendedparam') - prompt = self.xml.find('prompt') - rubric = self.xml.find('openendedrubric') - - #This is needed to attach feedback to specific responses later - self.submission_id=None - self.grader_id=None - - if oeparam is None: - raise ValueError("No oeparam found in problem xml.") - if prompt is None: - raise ValueError("No prompt found in problem xml.") - if rubric is None: - raise ValueError("No rubric found in problem xml.") - - self._parse(oeparam, prompt, rubric) - - @staticmethod - def stringify_children(node): - """ - Modify code from stringify_children in xmodule. Didn't import directly - in order to avoid capa depending on xmodule (seems to be avoided in - code) - """ - parts=[node.text if node.text is not None else ''] - for p in node.getchildren(): - parts.append(etree.tostring(p, with_tail=True, encoding='unicode')) - - return ' '.join(parts) - - def _parse(self, oeparam, prompt, rubric): - ''' - Parse OpenEndedResponse XML: - self.initial_display - self.payload - dict containing keys -- - 'grader' : path to grader settings file, 'problem_id' : id of the problem - - self.answer - What to display when show answer is clicked - ''' - # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload - prompt_string = self.stringify_children(prompt) - rubric_string = self.stringify_children(rubric) - - grader_payload = oeparam.find('grader_payload') - grader_payload = grader_payload.text if grader_payload is not None else '' - - #Update grader payload with student id. If grader payload not json, error. - try: - parsed_grader_payload = json.loads(grader_payload) - # NOTE: self.system.location is valid because the capa_module - # __init__ adds it (easiest way to get problem location into - # response types) - except TypeError, ValueError: - log.exception("Grader payload %r is not a json object!", grader_payload) - - self.initial_display = find_with_default(oeparam, 'initial_display', '') - self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') - - parsed_grader_payload.update({ - 'location' : self.system.location, - 'course_id' : self.system.course_id, - 'prompt' : prompt_string, - 'rubric' : rubric_string, - 'initial_display' : self.initial_display, - 'answer' : self.answer, - }) - updated_grader_payload = json.dumps(parsed_grader_payload) - - self.payload = {'grader_payload': updated_grader_payload} - - try: - self.max_score = int(find_with_default(oeparam, 'max_score', 1)) - except ValueError: - self.max_score = 1 - - def handle_message_post(self,event_info): - """ - Handles a student message post (a reaction to the grade they received from an open ended grader type) - Returns a boolean success/fail and an error message - """ - survey_responses=event_info['survey_responses'] - for tag in ['feedback', 'submission_id', 'grader_id', 'score']: - if tag not in survey_responses: - return False, "Could not find needed tag {0}".format(tag) - try: - submission_id=int(survey_responses['submission_id']) - grader_id = int(survey_responses['grader_id']) - feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) - score = int(survey_responses['score']) - except: - error_message=("Could not parse submission id, grader id, " - "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) - log.exception(error_message) - return False, "There was an error saving your feedback. Please contact course staff." - - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - anonymous_student_id = self.system.anonymous_student_id - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.answer_id) - - xheader = xqueue_interface.make_xheader( - lms_callback_url=self.system.xqueue['callback_url'], - lms_key=queuekey, - queue_name=self.message_queue_name - ) - - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime, - } - contents= { - 'feedback' : feedback, - 'submission_id' : submission_id, - 'grader_id' : grader_id, - 'score': score, - 'student_info' : json.dumps(student_info), - } - - (error, msg) = qinterface.send_to_queue(header=xheader, - body=json.dumps(contents)) - - #Convert error to a success value - success=True - if error: - success=False - - return success, "Successfully submitted your feedback." - - def get_score(self, student_answers): - - try: - submission = student_answers[self.answer_id] - except KeyError: - msg = ('Cannot get student answer for answer_id: {0}. student_answers {1}' - .format(self.answer_id, student_answers)) - log.exception(msg) - raise LoncapaProblemError(msg) - - # Prepare xqueue request - #------------------------------------------------------------ - - qinterface = self.system.xqueue['interface'] - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - - anonymous_student_id = self.system.anonymous_student_id - - # Generate header - queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + - self.answer_id) - - xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], - lms_key=queuekey, - queue_name=self.queue_name) - - self.context.update({'submission': submission}) - - contents = self.payload.copy() - - # Metadata related to the student submission revealed to the external grader - student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': qtime, - } - - #Update contents with student response and student info - contents.update({ - 'student_info': json.dumps(student_info), - 'student_response': submission, - 'max_score' : self.max_score, - }) - - # Submit request. When successful, 'msg' is the prior length of the queue - (error, msg) = qinterface.send_to_queue(header=xheader, - body=json.dumps(contents)) - - # State associated with the queueing request - queuestate = {'key': queuekey, - 'time': qtime,} - - cmap = CorrectMap() - if error: - cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: {0}.)' - ' Please try again later.'.format(msg)) - else: - # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that - # the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down - # through inputtypes.textbox and .filesubmission to inform the - # browser that the submission is queued (and it could e.g. poll) - cmap.set(self.answer_id, queuestate=queuestate, - correctness='incomplete', msg=msg) - - return cmap - - def update_score(self, score_msg, oldcmap, queuekey): - log.debug(score_msg) - score_msg = self._parse_score_msg(score_msg) - if not score_msg.valid: - oldcmap.set(self.answer_id, - msg = 'Invalid grader reply. Please contact the course staff.') - return oldcmap - - correctness = 'correct' if score_msg.correct else 'incorrect' - - # TODO: Find out how this is used elsewhere, if any - self.context['correct'] = correctness - - # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey - # does not match, we keep waiting for the score_msg whose key actually matches - if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points - points = score_msg.points - if points < 0: - points = 0 - - # Queuestate is consumed, so reset it to None - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, - msg = score_msg.msg.replace(' ', ' '), queuestate=None) - else: - log.debug('OpenEndedResponse: queuekey {0} does not match for answer_id={1}.'.format( - queuekey, self.answer_id)) - - return oldcmap - - def get_answers(self): - anshtml = '
{0}
'.format(self.answer) - return {self.answer_id: anshtml} - - def get_initial_display(self): - return {self.answer_id: self.initial_display} - - def _convert_longform_feedback_to_html(self, response_items): - """ - Take in a dictionary, and return html strings for display to student. - Input: - response_items: Dictionary with keys success, feedback. - if success is True, feedback should be a dictionary, with keys for - types of feedback, and the corresponding feedback values. - if success is False, feedback is actually an error string. - - NOTE: this will need to change when we integrate peer grading, because - that will have more complex feedback. - - Output: - String -- html that can be displayed to the student. - """ - - # We want to display available feedback in a particular order. - # This dictionary specifies which goes first--lower first. - priorities = {# These go at the start of the feedback - 'spelling': 0, - 'grammar': 1, - # needs to be after all the other feedback - 'markup_text': 3} - - default_priority = 2 - - def get_priority(elt): - """ - Args: - elt: a tuple of feedback-type, feedback - Returns: - the priority for this feedback type - """ - return priorities.get(elt[0], default_priority) - - def encode_values(feedback_type,value): - feedback_type=str(feedback_type).encode('ascii', 'ignore') - if not isinstance(value,basestring): - value=str(value) - value=value.encode('ascii', 'ignore') - return feedback_type,value - - def format_feedback(feedback_type, value): - feedback_type,value=encode_values(feedback_type,value) - feedback= """ -
- {value} -
- """.format(feedback_type=feedback_type, value=value) - return feedback - - def format_feedback_hidden(feedback_type , value): - feedback_type,value=encode_values(feedback_type,value) - feedback = """ - - """.format(feedback_type=feedback_type, value=value) - return feedback - - # TODO (vshnayder): design and document the details of this format so - # that we can do proper escaping here (e.g. are the graders allowed to - # include HTML?) - - for tag in ['success', 'feedback', 'submission_id', 'grader_id']: - if tag not in response_items: - return format_feedback('errors', 'Error getting feedback') - - feedback_items = response_items['feedback'] - try: - feedback = json.loads(feedback_items) - except (TypeError, ValueError): - log.exception("feedback_items have invalid json %r", feedback_items) - return format_feedback('errors', 'Could not parse feedback') - - if response_items['success']: - if len(feedback) == 0: - return format_feedback('errors', 'No feedback available') - - feedback_lst = sorted(feedback.items(), key=get_priority) - feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst) - else: - feedback_list_part1 = format_feedback('errors', response_items['feedback']) - - feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value) - for feedback_type,value in response_items.items() - if feedback_type in ['submission_id', 'grader_id']])) - - return u"\n".join([feedback_list_part1,feedback_list_part2]) - - def _format_feedback(self, response_items): - """ - Input: - Dictionary called feedback. Must contain keys seen below. - Output: - Return error message or feedback template - """ - - feedback = self._convert_longform_feedback_to_html(response_items) - - if not response_items['success']: - return self.system.render_template("open_ended_error.html", - {'errors' : feedback}) - - feedback_template = self.system.render_template("open_ended_feedback.html", { - 'grader_type': response_items['grader_type'], - 'score': "{0} / {1}".format(response_items['score'], self.max_score), - 'feedback': feedback, - }) - - return feedback_template - - - def _parse_score_msg(self, score_msg): - """ - Grader reply is a JSON-dump of the following dict - { 'correct': True/False, - 'score': Numeric value (floating point is okay) to assign to answer - 'msg': grader_msg - 'feedback' : feedback from grader - } - - Returns (valid_score_msg, correct, score, msg): - valid_score_msg: Flag indicating valid score_msg format (Boolean) - correct: Correctness of submission (Boolean) - score: Points to be assigned (numeric, can be float) - """ - fail = ScoreMessage(valid=False, correct=False, points=0, msg='') - try: - score_result = json.loads(score_msg) - except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) - return fail - - if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) - return fail - - for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: - if tag not in score_result: - log.error("External grader message is missing required tag: {0}" - .format(tag)) - return fail - - feedback = self._format_feedback(score_result) - self.submission_id=score_result['submission_id'] - self.grader_id=score_result['grader_id'] - - # HACK: for now, just assume it's correct if you got more than 2/3. - # Also assumes that score_result['score'] is an integer. - score_ratio = int(score_result['score']) / float(self.max_score) - correct = (score_ratio >= 0.66) - - #Currently ignore msg and only return feedback (which takes the place of msg) - return ScoreMessage(valid=True, correct=correct, - points=score_result['score'], msg=feedback) - -#----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration @@ -2261,5 +1831,4 @@ __all__ = [CodeResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, - JavascriptResponse, - OpenEndedResponse] + JavascriptResponse] diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index ba746fecb8..1c0ace9e59 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -25,7 +25,6 @@ class @Problem @$('section.action input.reset').click @reset @$('section.action input.show').click @show @$('section.action input.save').click @save - @$('section.evaluation input.submit-message').click @message_post # Collapsibles Collapsible.setCollapsibles(@el) @@ -198,35 +197,6 @@ class @Problem else @gentle_alert response.success - message_post: => - Logger.log 'message_post', @answers - - fd = new FormData() - feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value - submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML - grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML - score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() - fd.append('feedback', feedback) - fd.append('submission_id', submission_id) - fd.append('grader_id', grader_id) - if(!score) - @gentle_alert "You need to pick a rating before you can submit." - return - else - fd.append('score', score) - - - settings = - type: "POST" - data: fd - processData: false - contentType: false - success: (response) => - @gentle_alert response.message - @$('section.evaluation').slideToggle() - - $.ajaxWithPrefix("#{@url}/message_post", settings) - reset: => Logger.log 'problem_reset', @answers $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 682ba983bd..8a5ef42270 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -218,7 +218,7 @@ class @CombinedOpenEnded @reinitialize(@element) @rebind() @next_problem_button.hide() - if response.allow_reset=="False" + if !response.allow_reset @gentle_alert "Moved to next step." else @gentle_alert "Your score did not meet the criteria to move to the next step." diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0e16156f1a..024422773d 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -245,7 +245,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): that will have more complex feedback. Output: - String -- html that can be displayed to the student. + String -- html that can be displayincorrect-icon.pnged to the student. """ # We want to display available feedback in a particular order. From 2f841c8a334d329ea5d281668d714abd21c4d8d1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:55:28 -0500 Subject: [PATCH 174/329] Document combined open ended module --- .../xmodule/combined_open_ended_module.py | 144 ++++++++++++++++-- 1 file changed, 128 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index ee36690b1c..a639d6997a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -36,6 +36,10 @@ MAX_ATTEMPTS = 10000 MAX_SCORE = 1 class CombinedOpenEndedModule(XModule): + """ + This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). + It transitions between problems, and support arbitrary ordering. + """ STATE_VERSION = 1 # states @@ -59,16 +63,37 @@ class CombinedOpenEndedModule(XModule): instance_state, shared_state, **kwargs) """ - Definition file should have multiple task blocks: + Definition file should have one or many task blocks, a rubric block, and a prompt block: Sample file: - - - + + + Blah blah rubric. + + + Some prompt. + + + + What hint about this problem would you give to someone? + + + Save Succcesful. Thanks for participating! + + + + + Enter essay here. + This is the answer. + {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + + + + """ # Load instance state @@ -77,17 +102,19 @@ class CombinedOpenEndedModule(XModule): else: instance_state = {} - # History is a list of tuples of (answer, score, hint), where hint may be - # None for any element, and score and hint can be None for the last (current) - # element. - # Scores are on scale from 0 to max_score + #We need to set the location here so the child modules can use it system.set('location', location) - self.current_task_number = instance_state.get('current_task_number', 0) - self.task_states= instance_state.get('task_states', []) + #Tells the system which xml definition to load + self.current_task_number = instance_state.get('current_task_number', 0) + #This loads the states of the individual children + self.task_states= instance_state.get('task_states', []) + #Overall state of the combined open ended module self.state = instance_state.get('state', 'initial') self.attempts = instance_state.get('attempts', 0) + + #Allow reset is true if student has failed the criteria to move to the next child task self.allow_reset = instance_state.get('ready_to_reset', False) self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) @@ -95,6 +122,7 @@ class CombinedOpenEndedModule(XModule): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) + #Static data is passed to the child modules to render self.static_data = { 'max_score' : self._max_score, 'max_attempts' : self.max_attempts, @@ -106,10 +134,21 @@ class CombinedOpenEndedModule(XModule): self.setup_next_task() def get_tag_name(self, xml): + """ + Gets the tag name of a given xml block. + Input: XML string + Output: The name of the root tag + """ tag=etree.fromstring(xml).tag return tag def overwrite_state(self, current_task_state): + """ + Overwrites an instance state and sets the latest response to the current response. This is used + to ensure that the student response is carried over from the first child to the rest. + Input: Task state json string + Output: Task state json string + """ last_response_data=self.get_last_response(self.current_task_number-1) last_response = last_response_data['response'] @@ -122,6 +161,12 @@ class CombinedOpenEndedModule(XModule): return current_task_state def child_modules(self): + """ + Returns the functions associated with the child modules in a dictionary. This makes writing functions + simpler (saves code duplication) + Input: None + Output: A dictionary of dictionaries containing the descriptor functions and module functions + """ child_modules={ 'openended' : open_ended_module.OpenEndedModule, 'selfassessment' : self_assessment_module.SelfAssessmentModule, @@ -137,6 +182,12 @@ class CombinedOpenEndedModule(XModule): return children def setup_next_task(self, reset=False): + """ + Sets up the next task for the module. Creates an instance state if none exists, carries over the answer + from the last instance state to the next if needed. + Input: A boolean indicating whether or not the reset function is calling. + Output: Boolean True (not useful right now) + """ current_task_state=None if len(self.task_states)>self.current_task_number: current_task_state=self.task_states[self.current_task_number] @@ -176,6 +227,12 @@ class CombinedOpenEndedModule(XModule): return True def check_allow_reset(self): + """ + Checks to see if the student has passed the criteria to move to the next module. If not, sets + allow_reset to true and halts the student progress through the tasks. + Input: None + Output: the allow_reset attribute of the current module. + """ if not self.allow_reset: if self.current_task_number>0: last_response_data=self.get_last_response(self.current_task_number-1) @@ -188,6 +245,11 @@ class CombinedOpenEndedModule(XModule): return self.allow_reset def get_context(self): + """ + Generates a context dictionary that is used to render html. + Input: None + Output: A dictionary that can be rendered into the combined open ended template. + """ task_html=self.get_html_base() #set context variables and render template @@ -200,27 +262,47 @@ class CombinedOpenEndedModule(XModule): 'task_number' : self.current_task_number+1, 'status' : self.get_status(), } - log.debug(context) return context def get_html(self): + """ + Gets HTML for rendering. + Input: None + Output: rendered html + """ context=self.get_context() html = self.system.render_template('combined_open_ended.html', context) return html def get_html_nonsystem(self): + """ + Gets HTML for rendering via AJAX. Does not use system, because system contains some additional + html, which is not appropriate for returning via ajax calls. + Input: None + Output: HTML rendered directly via Mako + """ context=self.get_context() html = render_to_string('combined_open_ended.html', context) return html def get_html_base(self): + """ + Gets the HTML associated with the current child task + Input: None + Output: Child task HTML + """ self.update_task_states() html = self.current_task.get_html(self.system) return_html = rewrite_links(html, self.rewrite_content_links) return return_html def get_current_attributes(self, task_number): + """ + Gets the min and max score to attempt attributes of the specified task. + Input: The number of the task. + Output: The minimum and maximum scores needed to move on to the specified task. + """ task_xml=self.task_xml[task_number] etree_xml=etree.fromstring(task_xml) min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) @@ -228,6 +310,11 @@ class CombinedOpenEndedModule(XModule): return {'min_score_to_attempt' : min_score_to_attempt, 'max_score_to_attempt' : max_score_to_attempt} def get_last_response(self, task_number): + """ + Returns data associated with the specified task number, such as the last response, score, etc. + Input: The number of the task. + Output: A dictionary that contains information about the specified task. + """ last_response="" task_state = self.task_states[task_number] task_xml=self.task_xml[task_number] @@ -270,6 +357,11 @@ class CombinedOpenEndedModule(XModule): return last_response_dict def update_task_states(self): + """ + Updates the task state of the combined open ended module with the task state of the current child module. + Input: None + Output: boolean indicating whether or not the task state changed. + """ changed=False if not self.allow_reset: self.task_states[self.current_task_number] = self.current_task.get_instance_state() @@ -286,6 +378,11 @@ class CombinedOpenEndedModule(XModule): return changed def update_task_states_ajax(self,return_html): + """ + Runs the update task states function for ajax calls. Currently the same as update_task_states + Input: The html returned by the handle_ajax function of the child + Output: New html that should be rendered + """ changed=self.update_task_states() if changed: #return_html=self.get_html() @@ -293,6 +390,11 @@ class CombinedOpenEndedModule(XModule): return return_html def get_results(self, get): + """ + Gets the results of a given grader via ajax. + Input: AJAX get dictionary + Output: Dictionary to be rendered via ajax that contains the result html. + """ task_number=int(get['task_number']) self.update_task_states() response_dict=self.get_last_response(task_number) @@ -325,15 +427,19 @@ class CombinedOpenEndedModule(XModule): return json.dumps(d,cls=ComplexEncoder) def next_problem(self, get): + """ + Called via ajax to advance to the next problem. + Input: AJAX get request. + Output: Dictionary to be rendered + """ self.update_task_states() return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} def reset(self, get): """ - If resetting is allowed, reset the state. - - Returns {'success': bool, 'error': msg} - (error only present if not success) + If resetting is allowed, reset the state of the combined open ended module. + Input: AJAX get dictionary + Output: AJAX dictionary to tbe rendered """ if self.state != self.DONE: if not self.allow_reset: @@ -358,7 +464,9 @@ class CombinedOpenEndedModule(XModule): def get_instance_state(self): """ - Get the current score and state + Returns the current instance state. The module can be recreated from the instance state. + Input: None + Output: A dictionary containing the instance state. """ state = { @@ -373,6 +481,10 @@ class CombinedOpenEndedModule(XModule): return json.dumps(state) def get_status(self): + """ + Input: + Output: + """ status=[] for i in xrange(0,self.current_task_number+1): task_data = self.get_last_response(i) From 134c3f3db08250a3019591b287c868ee58ea1ec3 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 18:57:37 -0500 Subject: [PATCH 175/329] Document open ended descriptor --- .../xmodule/xmodule/combined_open_ended_module.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index a639d6997a..4bc0c1fc85 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -482,8 +482,9 @@ class CombinedOpenEndedModule(XModule): def get_status(self): """ - Input: - Output: + Gets the status panel to be displayed at the top right. + Input: None + Output: The status html to be rendered """ status=[] for i in xrange(0,self.current_task_number+1): @@ -497,7 +498,7 @@ class CombinedOpenEndedModule(XModule): class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ - Module for adding self assessment questions to courses + Module for adding combined open ended questions """ mako_template = "widgets/html-edit.html" module_class = CombinedOpenEndedModule @@ -513,14 +514,13 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): """ - Pull out the rubric, prompt, and submitmessage into a dictionary. + Pull out the individual tasks, the rubric, and the prompt, and parse Returns: { 'rubric': 'some-html', 'prompt': 'some-html', - 'submitmessage': 'some-html' - 'hintprompt': 'some-html' + 'task_xml': dictionary of xml strings, } """ expected_children = ['task', 'rubric', 'prompt'] From 5303086a1044fef7ce5da357335a9387a99b5c31 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:07:34 -0500 Subject: [PATCH 176/329] Start commenting open ended module --- .../lib/xmodule/xmodule/open_ended_module.py | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 024422773d..e4008309cd 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -37,8 +37,18 @@ from datetime import datetime log = logging.getLogger("mitx.courseware") class OpenEndedModule(openendedchild.OpenEndedChild): - + """ + The open ended module supports all external open ended grader problems. + """ def setup_response(self, system, location, definition, descriptor): + """ + Sets up the response type. + @param system: Modulesystem object + @param location: The location of the problem + @param definition: The xml definition of the problem + @param descriptor: The OpenEndedDescriptor associated with this + @return: None + """ oeparam = definition['oeparam'] self.url = definition.get('url', None) @@ -106,6 +116,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.payload = {'grader_payload': updated_grader_payload} def skip_post_assessment(self, get, system): + """ + Ajax function that allows one to skip the post assessment phase + @param get: AJAX dictionary + @param system: ModuleSystem + @return: Success indicator + """ self.state=self.DONE return {'success' : True} @@ -172,6 +188,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'success' : success, 'msg' : "Successfully submitted your feedback."} def send_to_grader(self, submission, system): + """ + Send a given submission to the grader, via the xqueue + @param submission: The student submission to send to the grader + @param system: Modulesystem + @return: Boolean true (not useful right now) + """ # Prepare xqueue request #------------------------------------------------------------ @@ -214,6 +236,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return True def _update_score(self, score_msg, queuekey, system): + """ + Called by xqueue to update the score + @param score_msg: The message from xqueue + @param queuekey: The key sent by xqueue + @param system: Modulesystem + @return: Boolean True (not useful currently) + """ new_score_msg = self._parse_score_msg(score_msg) if not new_score_msg['valid']: score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.' @@ -226,10 +255,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def get_answers(self): + """ + Gets and shows the answer for this problem. + @return: Answer html + """ anshtml = '
{0}
'.format(self.answer) return {self.answer_id: anshtml} def get_initial_display(self): + """ + Gets and shows the initial display for the input box. + @return: Initial display html + """ return {self.answer_id: self.initial_display} def _convert_longform_feedback_to_html(self, response_items): @@ -385,7 +422,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} def latest_post_assessment(self, short_feedback=False): - """None if not available""" + """ + Gets the latest feedback, parses, and returns + @param short_feedback: If the long feedback is wanted or not + @return: Returns formatted feedback + """ if not self.history: return "" @@ -397,6 +438,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return short_feedback if feedback_dict['valid'] else '' def format_feedback_with_evaluation(self,feedback): + """ + Renders a given html feedback into an evaluation template + @param feedback: HTML feedback + @return: Rendered html + """ context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} html= render_to_string('open_ended_evaluation.html', context) return html @@ -432,10 +478,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return json.dumps(d, cls=ComplexEncoder) def check_for_score(self, get, system): + """ + Checks to see if a score has been received yet. + @param get: AJAX get dictionary + @param system: Modulesystem (needed to align with other ajax functions) + @return: Returns the current state + """ state = self.state return {'state' : state} def save_answer(self, get, system): + """ + Saves a student answer + @param get: AJAX get dictionary + @param system: modulesystem + @return: Success indicator + """ if self.attempts > self.max_attempts: # If too many attempts, prevent student from saving answer and # seeing rubric. In normal use, students shouldn't see this because @@ -457,13 +515,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def update_score(self, get, system): """ - Delivers grading response (e.g. from asynchronous code checking) to - the capa problem, so its score can be updated - - 'get' must have a field 'response' which is a string that contains the - grader's response - - No ajax return is needed. Return empty dict. + Updates the current score via ajax. Called by xqueue. + Input: AJAX get dictionary, modulesystem + Output: None """ queuekey = get['queuekey'] score_msg = get['xqueue_body'] @@ -473,6 +527,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return dict() # No AJAX return is needed def get_html(self, system): + """ + Gets the HTML for this problem and renders it + Input: Modulesystem object + Output: Rendered HTML + """ #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() From f858dce753565e4555e875d01ea8909ff4599668 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:12:30 -0500 Subject: [PATCH 177/329] Document self assessment --- .../lib/xmodule/xmodule/open_ended_module.py | 14 +++++-- common/lib/xmodule/xmodule/openendedchild.py | 41 ++----------------- .../xmodule/xmodule/self_assessment_module.py | 37 +++++++++++++---- 3 files changed, 41 insertions(+), 51 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e4008309cd..ebd1cbfc02 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -39,6 +39,14 @@ log = logging.getLogger("mitx.courseware") class OpenEndedModule(openendedchild.OpenEndedChild): """ The open ended module supports all external open ended grader problems. + Sample XML file: + + + Enter essay here. + This is the answer. + {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + + """ def setup_response(self, system, location, definition, descriptor): """ @@ -562,7 +570,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ - Module for adding self assessment questions to courses + Module for adding open ended response questions to courses """ mako_template = "widgets/html-edit.html" module_class = OpenEndedModule @@ -578,12 +586,10 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): """ - Pull out the rubric, prompt, and submitmessage into a dictionary. + Pull out the open ended parameters into a dictionary. Returns: { - 'rubric': 'some-html', - 'prompt': 'some-html', 'oeparam': 'some-html' } """ diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 304271c620..5c2bdea76b 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -1,10 +1,3 @@ -""" -A Self Assessment module that allows students to write open-ended responses, -submit, then see a rubric and rate themselves. Persists student supplied -hints, answers, and assessment judgment (currently only correct/incorrect). -Parses xml definition file--see below for exact format. -""" - import copy from fs.errors import ResourceNotFoundError import itertools @@ -48,9 +41,9 @@ class OpenEndedChild(): initial (prompt, textbox shown) | - assessing (read-only textbox, rubric + assessment input shown) + assessing (read-only textbox, rubric + assessment input shown for self assessment, response queued for open ended) | - request_hint (read-only textbox, read-only rubric and assessment, hint input box shown) + post_assessment (read-only textbox, read-only rubric and assessment, hint input box shown) | done (submitted msg, green checkmark, everything else read-only. If attempts < max, shows a reset button that goes back to initial state. Saves previous @@ -69,6 +62,7 @@ class OpenEndedChild(): POST_ASSESSMENT = 'post_assessment' DONE = 'done' + #This is used to tell students where they are at in the module HUMAN_NAMES={ 'initial' : 'Started', 'assessing' : 'Being scored', @@ -78,35 +72,6 @@ class OpenEndedChild(): def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): - """ - Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, - and two optional attributes: - attempts, which should be an integer that defaults to 1. - If it's > 1, the student will be able to re-submit after they see - the rubric. - max_score, which should be an integer that defaults to 1. - It defines the maximum number of points a student can get. Assumed to be integer scale - from 0 to max_score, with an interval of 1. - - Note: all the submissions are stored. - - Sample file: - - - - Insert prompt text here. (arbitrary html) - - - Insert grading rubric here. (arbitrary html) - - - Please enter a hint below: (arbitrary html) - - - Thanks for submitting! (arbitrary html) - - - """ # Load instance state if instance_state is not None: diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 52701a8cf1..88632a38d0 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -1,10 +1,3 @@ -""" -A Self Assessment module that allows students to write open-ended responses, -submit, then see a rubric and rate themselves. Persists student supplied -hints, answers, and assessment judgment (currently only correct/incorrect). -Parses xml definition file--see below for exact format. -""" - import copy from fs.errors import ResourceNotFoundError import itertools @@ -31,14 +24,42 @@ import openendedchild log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): + """ + A Self Assessment module that allows students to write open-ended responses, + submit, then see a rubric and rate themselves. Persists student supplied + hints, answers, and assessment judgment (currently only correct/incorrect). + Parses xml definition file--see below for exact format. + Sample XML format: + + + What hint about this problem would you give to someone? + + + Save Succcesful. Thanks for participating! + + + """ def setup_response(self, system, location, definition, descriptor): + """ + Sets up the module + @param system: Modulesystem + @param location: location, to let the module know where it is. + @param definition: XML definition of the module. + @param descriptor: SelfAssessmentDescriptor + @return: None + """ self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] self.prompt = stringify_children(self.prompt) self.rubric = stringify_children(self.rubric) def get_html(self, system): + """ + Gets context and renders HTML that represents the module + @param system: Modulesystem + @return: Rendered HTML + """ #set context variables and render template if self.state != self.INITIAL: latest = self.latest_answer() @@ -266,8 +287,6 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): Returns: { - 'rubric': 'some-html', - 'prompt': 'some-html', 'submitmessage': 'some-html' 'hintprompt': 'some-html' } From 4a2875ccdd302fa1d08990ed02912dd2e2182c0e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 8 Jan 2013 19:15:57 -0500 Subject: [PATCH 178/329] Document open ended child --- common/lib/xmodule/xmodule/openendedchild.py | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index 5c2bdea76b..ce1b15074f 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -72,7 +72,6 @@ class OpenEndedChild(): def __init__(self, system, location, definition, descriptor, static_data, instance_state=None, shared_state=None, **kwargs): - # Load instance state if instance_state is not None: instance_state = json.loads(instance_state) @@ -102,6 +101,14 @@ class OpenEndedChild(): self.setup_response(system, location, definition, descriptor) def setup_response(self, system, location, definition, descriptor): + """ + Needs to be implemented by the inheritors of this module. Sets up additional fields used by the child modules. + @param system: Modulesystem + @param location: Module location + @param definition: XML definition + @param descriptor: Descriptor of the module + @return: None + """ pass def latest_answer(self): @@ -123,6 +130,11 @@ class OpenEndedChild(): return self.history[-1].get('post_assessment', "") def new_history_entry(self, answer): + """ + Adds a new entry to the history dictionary + @param answer: The student supplied answer + @return: None + """ self.history.append({'answer': answer}) def record_latest_score(self, score): @@ -213,12 +225,25 @@ class OpenEndedChild(): 'error': 'The problem state got out-of-sync'} def get_html(self): + """ + Needs to be implemented by inheritors. Renders the HTML that students see. + @return: + """ pass def handle_ajax(self): + """ + Needs to be implemented by child modules. Handles AJAX events. + @return: + """ pass def is_submission_correct(self, score): + """ + Checks to see if a given score makes the answer correct. Very naive right now (>66% is correct) + @param score: Numeric score. + @return: Boolean correct. + """ correct=False if(isinstance(score,(int, long, float, complex))): score_ratio = int(score) / float(self.max_score()) @@ -226,6 +251,10 @@ class OpenEndedChild(): return correct def is_last_response_correct(self): + """ + Checks to see if the last response in the module is correct. + @return: 'correct' if correct, otherwise 'incorrect' + """ score=self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' return correctness From 5a3a537c1b1668eec294966d5d1ea15cdc43543a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 13:00:37 -0500 Subject: [PATCH 179/329] Support formatting of peer grading feedback --- .../lib/xmodule/xmodule/open_ended_module.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ebd1cbfc02..0d30950592 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -422,8 +422,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild): log.error("External grader message is missing required tag: {0}" .format(tag)) return fail - - feedback = self._format_feedback(score_result) + #This is to support peer grading + if isinstance(score_result['score'], list): + feedback_items=[] + for i in xrange(0,len(score_result['score'])): + new_score_result={ + 'score' : score_result['score'][i], + 'feedback' : score_result['feedback'][i], + 'grader_type' : score_result['grader_type'], + 'success' : score_result['success'], + 'grader_id' : score_result['grader_id'][i], + 'submission_id' : score_result['submission_id'] + } + feedback_items.append(self._format_feedback(new_score_result)) + feedback="".join(feedback_items) + else: + feedback = self._format_feedback(score_result) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From c267efb40099d6fc2e3508257caa21e94378b800 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 13:03:01 -0500 Subject: [PATCH 180/329] Add in a comment --- common/lib/xmodule/xmodule/open_ended_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0d30950592..1ce1e09e03 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -437,6 +437,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) else: + #This is for instructor and ML grading feedback = self._format_feedback(score_result) self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From c4b1c8d074bdb4efe1588acc474f26a2635e61c9 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:29:49 -0500 Subject: [PATCH 181/329] Correct peer grading score parsing --- common/lib/xmodule/xmodule/open_ended_module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 1ce1e09e03..07ceb79b1a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -31,6 +31,7 @@ from capa.util import * import openendedchild from mitxmako.shortcuts import render_to_string +from numpy import median from datetime import datetime @@ -436,9 +437,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): } feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) + score = median(score_result['score']) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) + self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] From 742d9475c79ae1faa7d6043e3597d70ece7cef11 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:31:02 -0500 Subject: [PATCH 182/329] Parse int from score --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 07ceb79b1a..ac852e4e5e 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -437,7 +437,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): } feedback_items.append(self._format_feedback(new_score_result)) feedback="".join(feedback_items) - score = median(score_result['score']) + score = int(median(score_result['score'])) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) From 62e93870957c01e0e6b013103c2c06dee4bb6d4d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 14:42:29 -0500 Subject: [PATCH 183/329] Fix score passing from controller --- common/lib/xmodule/xmodule/open_ended_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index ac852e4e5e..2a253c663f 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -441,11 +441,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) + score=score_result['score'] self.submission_id=score_result['submission_id'] self.grader_id=score_result['grader_id'] - return {'valid' : True, 'score' : score_result['score'], 'feedback' : feedback} + return {'valid' : True, 'score' : score, 'feedback' : feedback} def latest_post_assessment(self, short_feedback=False): """ From cb203a6f5532ec052c668fd5969261385fce56cc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 15:54:15 -0500 Subject: [PATCH 184/329] Better error messages --- .../xmodule/combined_open_ended_module.py | 1 + .../lib/xmodule/xmodule/open_ended_module.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 4bc0c1fc85..42f2393ad9 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -224,6 +224,7 @@ class CombinedOpenEndedModule(XModule): current_task_state=self.overwrite_state(current_task_state) self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + log.debug(current_task_state) return True def check_allow_reset(self): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 2a253c663f..367b5d9e67 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -405,23 +405,29 @@ class OpenEndedModule(openendedchild.OpenEndedChild): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) """ - fail = {'valid' : False, 'correct' : False, 'points' : 0, 'msg' : ''} + fail = {'valid' : False, 'score' : 0, 'feedback' : ''} try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) + error_message=("External grader message should be a JSON-serialized dict." + " Received score_msg = {0}".format(score_msg)) + log.error(error_message) + fail['feedback']=error_message return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) + error_message=("External grader message should be a JSON-serialized dict." + " Received score_result = {0}".format(score_result)) + log.error(error_message) + fail['feedback']=error_message return fail for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: if tag not in score_result: - log.error("External grader message is missing required tag: {0}" + error_message=("External grader message is missing required tag: {0}" .format(tag)) + log.error(error_message) + fail['feedback']=error _message return fail #This is to support peer grading if isinstance(score_result['score'], list): From f4968e1e8e1b80677130a662b5adb5c85c6a7f44 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:01:03 -0500 Subject: [PATCH 185/329] Fix spacing error --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 367b5d9e67..0420faf534 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -427,7 +427,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): error_message=("External grader message is missing required tag: {0}" .format(tag)) log.error(error_message) - fail['feedback']=error _message + fail['feedback']=error_message return fail #This is to support peer grading if isinstance(score_result['score'], list): From b0d3bcc5166feced1285826f745e42c8bd5ed871 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:37:15 -0500 Subject: [PATCH 186/329] Fix feedback response for peer grading so that students can respond to multiple feedback items --- .../capa/capa/templates/openendedinput.html | 56 ----------------- .../xmodule/combined_open_ended_module.py | 10 ++- .../js/src/combinedopenended/display.coffee | 62 ++++++++++--------- .../lib/xmodule/xmodule/open_ended_module.py | 11 ++-- 4 files changed, 48 insertions(+), 91 deletions(-) delete mode 100644 common/lib/capa/capa/templates/openendedinput.html diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html deleted file mode 100644 index c42ad73faf..0000000000 --- a/common/lib/capa/capa/templates/openendedinput.html +++ /dev/null @@ -1,56 +0,0 @@ -
- - -
- % if status == 'unsubmitted': - Unanswered - % elif status == 'correct': - Correct - % elif status == 'incorrect': - Incorrect - % elif status == 'queued': - Submitted for grading - % endif - - % if hidden: -
- % endif -
- - - - % if status == 'queued': - - % endif -
- ${msg|n} - % if status in ['correct','incorrect']: -
-
- Respond to Feedback -
-
-

How accurate do you find this feedback?

-
-
    -
  • -
  • -
  • -
  • -
  • -
-
-

Additional comments:

- -
- -
-
-
- % endif -
-
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 42f2393ad9..244346625a 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -336,8 +336,14 @@ class CombinedOpenEndedModule(XModule): last_post_assessment = task.latest_post_assessment() last_post_feedback="" if task_type=="openended": - last_post_assessment = task.latest_post_assessment(short_feedback=False) - last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) + last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) + if isinstance(last_post_assessment,list): + eval_list=[] + for i in xrange(0,len(last_post_assessment)): + eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) + last_post_evaluation="".join(eval_list) + else: + last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation last_correctness = task.is_last_response_correct() max_score = task.max_score() diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 8a5ef42270..5e2d2db86e 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -69,6 +69,39 @@ class @CombinedOpenEnded else @errors_area.html(response.error) + message_post: (event)=> + Logger.log 'message_post', @answers + external_grader_message=$(event.target).parent().parent().parent() + evaluation_scoring = $(event.target).parent() + + fd = new FormData() + feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value + submission_id = external_grader_message.find('div.submission_id')[0].innerHTML + grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() + + fd.append('feedback', feedback) + fd.append('submission_id', submission_id) + fd.append('grader_id', grader_id) + if(!score) + @gentle_alert "You need to pick a rating before you can submit." + return + else + fd.append('score', score) + + settings = + type: "POST" + data: fd + processData: false + contentType: false + success: (response) => + @gentle_alert response.msg + $('section.evaluation').slideToggle() + @message_wrapper.html(response.message_html) + + $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) + + rebind: () => # rebind to the appropriate function for the current state @submit_button.unbind('click') @@ -227,35 +260,6 @@ class @CombinedOpenEnded else @errors_area.html('Problem state got out of sync. Try reloading the page.') - message_post: => - Logger.log 'message_post', @answers - - fd = new FormData() - feedback = $('section.evaluation textarea.feedback-on-feedback')[0].value - submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML - grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML - score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val() - fd.append('feedback', feedback) - fd.append('submission_id', submission_id) - fd.append('grader_id', grader_id) - if(!score) - @gentle_alert "You need to pick a rating before you can submit." - return - else - fd.append('score', score) - - settings = - type: "POST" - data: fd - processData: false - contentType: false - success: (response) => - @gentle_alert response.msg - $('section.evaluation').slideToggle() - @message_wrapper.html(response.message_html) - - $.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings) - gentle_alert: (msg) => if @el.find('.open-ended-alert').length @el.find('.open-ended-alert').remove() diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 0420faf534..f715c9d76a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -391,7 +391,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return feedback_template - def _parse_score_msg(self, score_msg): + def _parse_score_msg(self, score_msg, join_feedback=True): """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, @@ -442,7 +442,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'submission_id' : score_result['submission_id'] } feedback_items.append(self._format_feedback(new_score_result)) - feedback="".join(feedback_items) + if join_feedback: + feedback="".join(feedback_items) + else: + feedback=feedback_items score = int(median(score_result['score'])) else: #This is for instructor and ML grading @@ -454,7 +457,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return {'valid' : True, 'score' : score, 'feedback' : feedback} - def latest_post_assessment(self, short_feedback=False): + def latest_post_assessment(self, short_feedback=False, join_feedback=True): """ Gets the latest feedback, parses, and returns @param short_feedback: If the long feedback is wanted or not @@ -463,7 +466,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not self.history: return "" - feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', "")) + feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback) if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' if feedback_dict['valid']: From e4568c3a2061b7ba17a6a1af45fac9f87e955762 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 16:41:17 -0500 Subject: [PATCH 187/329] Use include clearfix in css --- .../xmodule/xmodule/css/combinedopenended/display.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index be86757aee..8ebb3a2888 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -21,6 +21,7 @@ h2 { } section.combined-open-ended { + @include clearfix; .status-container { float:right; @@ -40,14 +41,6 @@ section.combined-open-ended { position:relative; } - &:after - { - content:"."; - display:block; - height:0; - visibility: hidden; - clear:both; - } } section.combined-open-ended-status { From ee2990da4f0c8151a00f2fb7ed401c44344d860c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:05:08 -0500 Subject: [PATCH 188/329] Change hidden div to input name --- common/lib/xmodule/xmodule/css/combinedopenended/display.scss | 1 - .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 8ebb3a2888..b5eb4b52e6 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -40,7 +40,6 @@ section.combined-open-ended { width: 93%; position:relative; } - } section.combined-open-ended-status { diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 5e2d2db86e..29b0424a6d 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('div.submission_id')[0].innerHTML - grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + submission_id = external_grader_message.find('input.submission_id')[0].innerHTML + grader_id = external_grader_message.find('input.grader_id')[0].innerHTML score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index f715c9d76a..3f5d5b0110 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From c76786ae4ee6ad63a88ed875e601884c24dcebdd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:08:10 -0500 Subject: [PATCH 189/329] Roll back hidden input changes --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 29b0424a6d..5e2d2db86e 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('input.submission_id')[0].innerHTML - grader_id = external_grader_message.find('input.grader_id')[0].innerHTML + submission_id = external_grader_message.find('div.submission_id')[0].innerHTML + grader_id = external_grader_message.find('div.grader_id')[0].innerHTML score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 3f5d5b0110..f715c9d76a 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From 8dbbb021a730256ef0ba3a9f04d58085d78ad9f2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 9 Jan 2013 17:23:33 -0500 Subject: [PATCH 190/329] Change value passing to hidden input type --- .../xmodule/xmodule/js/src/combinedopenended/display.coffee | 4 ++-- common/lib/xmodule/xmodule/open_ended_module.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 5e2d2db86e..2cbba143a3 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -76,8 +76,8 @@ class @CombinedOpenEnded fd = new FormData() feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value - submission_id = external_grader_message.find('div.submission_id')[0].innerHTML - grader_id = external_grader_message.find('div.grader_id')[0].innerHTML + submission_id = external_grader_message.find('input.submission_id')[0].value + grader_id = external_grader_message.find('input.grader_id')[0].value score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val() fd.append('feedback', feedback) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index f715c9d76a..5649cbbd2c 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -332,9 +332,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def format_feedback_hidden(feedback_type , value): feedback_type,value=encode_values(feedback_type,value) feedback = """ - + """.format(feedback_type=feedback_type, value=value) return feedback From 200493a54f4e7c4ffdd6a7c6d307098ff4aa7782 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 6 Jan 2013 20:57:44 +0000 Subject: [PATCH 191/329] instructor dashboard upgrade - add enrollment management --- ...ed__add_unique_courseenrollmentallowed_.py | 155 +++++++++++++ .../migrations/0021_remove_askbot.py.old | 157 ++++++++++++++ common/djangoapps/student/models.py | 18 ++ lms/djangoapps/instructor/views.py | 205 ++++++++++++++++-- lms/envs/dev.py | 4 + .../courseware/instructor_dashboard.html | 51 ++++- 6 files changed, 567 insertions(+), 23 deletions(-) create mode 100644 common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py create mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py new file mode 100644 index 0000000000..f7e2571685 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseEnrollmentAllowed' + db.create_table('student_courseenrollmentallowed', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + )) + db.send_create_signal('student', ['CourseEnrollmentAllowed']) + + # Adding unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.create_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.delete_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + # Deleting model 'CourseEnrollmentAllowed' + db.delete_table('student_courseenrollmentallowed') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] \ No newline at end of file diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old new file mode 100644 index 0000000000..89f7208f40 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_remove_askbot.py.old @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +ASKBOT_AUTH_USER_COLUMNS = ( + 'website', + 'about', + 'gold', + 'email_isvalid', + 'real_name', + 'location', + 'reputation', + 'gravatar', + 'bronze', + 'last_seen', + 'silver', + 'questions_per_page', + 'new_response_count', + 'seen_response_count', +) + + +class Migration(SchemaMigration): + + def forwards(self, orm): + "Kill the askbot" + # For MySQL, we're batching the alters together for performance reasons + if db.backend_name == 'mysql': + drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] + statement = "alter table `auth_user` {0};".format(", ".join(drops)) + db.execute(statement) + else: + for column in ASKBOT_AUTH_USER_COLUMNS: + db.delete_column('auth_user', column) + + def backwards(self, orm): + raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 2f5bc3ac04..d3254532bc 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -262,6 +262,24 @@ class CourseEnrollment(models.Model): return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) +class CourseEnrollmentAllowed(models.Model): + """ + Table of users (specified by email address strings) who are allowed to enroll in a specified course. + The user may or may not (yet) exist. Enrollment by users listed in this table is allowed + even if the enrollment time window is past. + """ + email = models.CharField(max_length=255, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + + class Meta: + unique_together = (('email', 'course_id'), ) + + def __unicode__(self): + return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) + + @receiver(post_save, sender=CourseEnrollment) def assign_default_role(sender, instance, **kwargs): if instance.user.is_staff: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2bad058ad8..b74fd495a1 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -2,8 +2,10 @@ from collections import defaultdict import csv +import json import logging import os +import requests import urllib from django.conf import settings @@ -20,7 +22,7 @@ from courseware.courses import get_course_with_access from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseEnrollmentAllowed from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -125,7 +127,7 @@ def instructor_dashboard(request, course_id): except Exception as err: msg += '

Error: {0}

'.format(escape(err)) - if action == 'Dump list of enrolled students': + if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) @@ -257,6 +259,70 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id), {}, page='idashboard') + #---------------------------------------- + # enrollment + + elif action == 'List students who may enroll but may not have yet signed up': + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + datatable = {'header': ['StudentEmail']} + datatable['data'] = [[x.email] for x in ceaset] + datatable['title'] = action + + elif action == 'Enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) + nce.save() + msg += "Enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) + nce.delete() + msg += "Un-enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll ALL students': + + ret = _do_enroll_students(course, course_id, '', overload=True) + datatable = ret['datatable'] + + elif action == 'Enroll multiple students': + + students = request.POST.get('enroll_multiple','') + ret = _do_enroll_students(course, course_id, students) + datatable = ret['datatable'] + + elif action == 'List sections available in remote gradebook': + + msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg += msg2 + + elif action in ['List students in section in remote gradebook', + 'Overload enrollment list using remote gradebook', + 'Merge enrollment list with remote gradebook']: + + section = request.POST.get('gradebook_section','') + msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg += msg2 + + if not 'List' in action: + students = ','.join([x['email'] for x in datatable['retdata']]) + overload = 'Overload' in action + ret = _do_enroll_students(course, course_id, students, overload=overload) + datatable = ret['datatable'] + + #---------------------------------------- # psychometrics @@ -270,9 +336,9 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) - #---------------------------------------- # context for rendering + context = {'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, @@ -285,16 +351,65 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') } return render_to_response('courseware/instructor_dashboard.html', context) + +def _do_remote_gradebook(course, action, args=None): + ''' + Perform remote gradebook action. Returns msg, datatable. + ''' + rg = course.metadata.get('remote_gradebook','') + if not rg: + msg = "No remote gradebook defined in course metadata" + return msg, {} + + rgurl = settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') + if not rgurl: + msg = "No remote gradebook url defined in settings.MITX_FEATURES" + return msg, {} + + rgname = rg.get('name','') + if not rgname: + msg = "No gradebook name defined in course remote_gradebook metadata" + return msg, {} + + if args is None: + args = {} + data = dict(submit=action, gradebook=rgname) + data.update(args) + + try: + resp = requests.post(rgurl, data=data, verify=False) + retdict = json.loads(resp.content) + except Exception as err: + msg = "Failed to communicate with gradebook server at %s
" % rgurl + msg += "Error: %s" % err + msg += "
resp=%s" % resp.content + msg += "
data=%s" % data + return msg, {} + + msg = '
%s
' % retdict['msg'].replace('\n','
') + retdata = retdict['data'] + + if retdata: + datatable = {'header': retdata[0].keys()} + datatable['data'] = [x.values() for x in retdata] + datatable['title'] = 'Remote gradebook response for %s' % action + datatable['retdata'] = retdata + else: + datatable = {} + + return msg, datatable + def _list_course_forum_members(course_id, rolename, datatable): ''' Fills in datatable with forum membership information, for a given role, so that it will be displayed on instructor dashboard. - course_ID = course's ID string + course_ID = the ID string for a course rolename = one of "Administrator", "Moderator", "Community TA" Returns message status string to append to displayed message, if role is unknown. @@ -455,6 +570,68 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +def _do_enroll_students(course, course_id, students, overload=False): + """Do the actual work of enrolling multiple students, presented as a string + of emails separated by commas or returns""" + + ns = [x.split('\n') for x in students.split(',')] + new_students = [item for sublist in ns for item in sublist] + new_students = [str(s.strip()) for s in new_students] + new_students_lc = [x.lower() for x in new_students] + + if '' in new_students: + new_students.remove('') + + status = dict([x,'unprocessed'] for x in new_students) + + if overload: # delete all but staff + todelete = CourseEnrollment.objects.filter(course_id=course_id) + for ce in todelete: + if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc: + status[ce.user.email] = 'deleted' + ce.delete() + else: + status[ce.user.email] = 'is staff' + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + for cea in ceaset: + status[cea.email] = 'removed from pending enrollment list' + ceaset.delete() + + for student in new_students: + try: + user=User.objects.get(email=student) + except User.DoesNotExist: + # user not signed up yet, put in pending enrollment allowed table + if CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id): + status[student] = 'user does not exist, enrollment already allowed, pending' + continue + cea = CourseEnrollmentAllowed(email=student, course_id=course_id) + cea.save() + status[student] = 'user does not exist, enrollment allowed, pending' + continue + + if CourseEnrollment.objects.filter(user=user, course_id=course_id): + status[student] = 'already enrolled' + continue + try: + nce = CourseEnrollment(user=user, course_id=course_id) + nce.save() + status[student] = 'added' + except: + status[student] = 'rejected' + + datatable = {'header': ['StudentEmail', 'action']} + datatable['data'] = [[x, status[x]] for x in status] + datatable['title'] = 'Enrollment of students' + + def sf(stat): return [x for x in status if status[x]==stat] + + data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'), + deleted=sf('deleted'), datatable=datatable) + + return data + + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def enroll_students(request, course_id): @@ -473,22 +650,10 @@ def enroll_students(request, course_id): course = get_course_with_access(request.user, course_id, 'staff') existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)] - if 'new_students' in request.POST: - new_students = request.POST['new_students'].split('\n') - else: - new_students = [] - new_students = [s.strip() for s in new_students] - - added_students = [] - rejected_students = [] - - for student in new_students: - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - added_students.append(student) - except: - rejected_students.append(student) + new_students = request.POST.get('new_students') + ret = _do_enroll_students(course, course_id, new_students) + added_students = ret['added'] + rejected_students = ret['rejected'] return render_to_response("enroll_students.html", {'course': course_id, 'existing_students': existing_students, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 058c67fa4d..f5999bf52e 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = { COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" +################################# mitx revision string ##################### + +MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip() + ################################# Staff grading config ##################### STAFF_GRADING_INTERFACE = { diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 74bc25fcbe..bb0dcef970 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -57,10 +57,13 @@ function goto( mode) Psychometrics | %endif Admin | - Forum Admin ] + Forum Admin | + Enrollment + ] -
${djangopid}
+
${djangopid} + | ${mitx_version}
@@ -163,10 +166,52 @@ function goto( mode) %endif %endif +##----------------------------------------------------------------------------- +%if modeflag.get('Enrollment'): + +
+

+ + +

+ Student Email: + + +


+ + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

Pull enrollment from remote gradebook

+
    +
  • Gradebook name:
  • +
  • Section:
  • +
+ + + + +
+ + %endif + +

Add students: enter emails, separated by returns or commas;

+ + + +%endif + +##----------------------------------------------------------------------------- +
##----------------------------------------------------------------------------- -%if modeflag.get('Psychometrics') is None: +##----------------------------------------------------------------------------- + +%if datatable and modeflag.get('Psychometrics') is None:

From 97fb05444922c6def563e5fb56c0f3d4599ea0a4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 00:50:59 +0000 Subject: [PATCH 192/329] add export grades to remote gradebook to instructor dashboard --- lms/djangoapps/instructor/views.py | 96 ++++++++++++++++--- .../courseware/instructor_dashboard.html | 38 +++++++- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b74fd495a1..b5caeac964 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -8,6 +8,8 @@ import os import requests import urllib +from StringIO import StringIO + from django.conf import settings from django.contrib.auth.models import User, Group from django.http import HttpResponse @@ -77,9 +79,12 @@ def instructor_dashboard(request, course_id): data.append(['metadata', escape(str(course.metadata))]) datatable['data'] = data - def return_csv(fn, datatable): - response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + def return_csv(fn, datatable, fp=None): + if fp is None: + response = HttpResponse(mimetype='text/csv') + response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + else: + response = fp writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: @@ -160,6 +165,65 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) + #---------------------------------------- + # export grades to remote gradebook + + elif action=='List assignments available in remote gradebook': + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') + msg += msg2 + + elif action=='List assignments available for this course': + log.debug(action) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + + assignments = [[x] for x in allgrades['assignments']] + datatable = {'header': ['Assignment Name']} + datatable['data'] = assignments + datatable['title'] = action + + msg += 'assignments=
%s
' % assignments + + elif action=='List enrolled students matching remote gradebook': + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') + datatable = {'header': ['Student email', 'Match?']} + rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] + def domatch(x): + return 'yes' if x.email in rg_students else 'No' + datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] + datatable['title'] = action + + elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', + 'Export CSV file of grades for assignment']: + + log.debug(action) + datatable = {} + aname = request.POST.get('assignment_name','') + if not aname: + msg += "Please enter an assignment name" + else: + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + if aname not in allgrades['assignments']: + msg += "Invalid assignment name '%s'" % aname + else: + aidx = allgrades['assignments'].index(aname) + datatable = {'header': ['External email', aname]} + datatable['data'] = [[x.email, x.grades[aidx]] for x in allgrades['students']] + datatable['title'] = 'Grades for assignment "%s"' % aname + + if 'Export CSV' in action: + # generate and return CSV file + return return_csv('grades %s.csv' % aname, datatable) + + elif 'remote gradebook' in action: + fp = StringIO() + return_csv('', datatable, fp=fp) + fp.seek(0) + files = {'datafile': fp} + msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files) + msg += msg2 + + #---------------------------------------- # Admin @@ -305,7 +369,7 @@ def instructor_dashboard(request, course_id): elif action == 'List sections available in remote gradebook': - msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', @@ -313,7 +377,7 @@ def instructor_dashboard(request, course_id): 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section','') - msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section) ) msg += msg2 if not 'List' in action: @@ -357,7 +421,7 @@ def instructor_dashboard(request, course_id): return render_to_response('courseware/instructor_dashboard.html', context) -def _do_remote_gradebook(course, action, args=None): +def _do_remote_gradebook(user, course, action, args=None, files=None): ''' Perform remote gradebook action. Returns msg, datatable. ''' @@ -378,11 +442,11 @@ def _do_remote_gradebook(course, action, args=None): if args is None: args = {} - data = dict(submit=action, gradebook=rgname) + data = dict(submit=action, gradebook=rgname, user=user.email) data.update(args) try: - resp = requests.post(rgurl, data=data, verify=False) + resp = requests.post(rgurl, data=data, verify=False, files=files) retdict = json.loads(resp.content) except Exception as err: msg = "Failed to communicate with gradebook server at %s
" % rgurl @@ -392,7 +456,7 @@ def _do_remote_gradebook(course, action, args=None): return msg, {} msg = '
%s
' % retdict['msg'].replace('\n','
') - retdata = retdict['data'] + retdata = retdict['data'] # a list of dicts if retdata: datatable = {'header': retdata[0].keys()} @@ -495,16 +559,18 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] + assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: - header += [score.section for score in gradeset['raw_scores']] + assignments += [score.section for score in gradeset['raw_scores']] else: - header += [x['label'] for x in gradeset['section_breakdown']] + assignments += [x['label'] for x in gradeset['section_breakdown']] + header += assignments - datatable = {'header': header} + datatable = {'header': header, 'assignments': assignments, 'students': enrolled_students} data = [] for student in enrolled_students: @@ -518,9 +584,11 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - datarow += [score.earned for score in gradeset['raw_scores']] + student_grades = [score.earned for score in gradeset['raw_scores']] else: - datarow += [x['percent'] for x in gradeset['section_breakdown']] + student_grades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += student_grades + student.grades = student_grades # store in student object data.append(datarow) datatable['data'] = data diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index bb0dcef970..b2ec220484 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -96,6 +96,42 @@ function goto( mode)

+
+ + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

Export grades to remote gradebook

+

The assignments defined for this course should match the ones + stored in the gradebook, for this to work properly!

+ +
    +
  • Gradebook name: ${rg.get('name','None defined!')} +
    +
    + + +
    +
    +
  • +
  • +
    +
    +
  • +
  • Assignment name: +
    +
    + + + +
  • +
+ + %endif + %endif ##----------------------------------------------------------------------------- @@ -187,7 +223,7 @@ function goto( mode)

Pull enrollment from remote gradebook

    -
  • Gradebook name:
  • +
  • Gradebook name: ${rg.get('name','None defined!')}
  • Section:
From 82e31d533b1640836b683e9f408b8d9afb5cc6e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 02:03:27 +0000 Subject: [PATCH 193/329] Hookup CourseEnrollmentAllowed to lms/djangoapps/courseware/access.py --- lms/djangoapps/courseware/access.py | 6 ++++++ lms/djangoapps/instructor/views.py | 23 ++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ba9b8a3bc0..0d4a37eda5 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,6 +5,8 @@ like DISABLE_START_DATES""" import logging import time +import student.models + from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -124,6 +126,10 @@ def _has_access_course_desc(user, course, action): debug("Allow: in enrollment period") return True + # if user is in CourseEnrollmentAllowed with right course_id then can also enroll + if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True + # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b5caeac964..0ea3cf0435 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -335,26 +335,27 @@ def instructor_dashboard(request, course_id): elif action == 'Enroll student': student = request.POST.get('enstudent','') - datatable = {} - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - msg += "Enrolled student with email '%s'" % student - except Exception as err: - msg += "Error! Failed to enroll student with email '%s'\n" % student - msg += str(err) + '\n' + ret = _do_enroll_students(course, course_id, student) + datatable = ret['datatable'] elif action == 'Un-enroll student': student = request.POST.get('enstudent','') datatable = {} + isok = False + cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student) + if cea: + cea.delete() + msg += "Un-enrolled student with email '%s'" % student + isok = True try: nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) nce.delete() msg += "Un-enrolled student with email '%s'" % student except Exception as err: - msg += "Error! Failed to un-enroll student with email '%s'\n" % student - msg += str(err) + '\n' + if not isok: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' elif action == 'Un-enroll ALL students': @@ -582,7 +583,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, if get_grades: gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) - # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) + log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: student_grades = [score.earned for score in gradeset['raw_scores']] else: From 04d6f08c0cd4c901649c90edc0bca71424e2af7b Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:17:02 +0000 Subject: [PATCH 194/329] add offline grade computation & DB table for this --- common/djangoapps/student/admin.py | 2 + lms/djangoapps/courseware/access.py | 14 ++- lms/djangoapps/courseware/admin.py | 5 + ..._add_unique_offlinecomputedgrade_user_c.py | 117 ++++++++++++++++++ lms/djangoapps/courseware/models.py | 37 ++++++ .../management/commands/compute_grades.py | 50 ++++++++ .../instructor/offline_gradecalc.py | 103 +++++++++++++++ lms/djangoapps/instructor/views.py | 56 ++++++--- .../courseware/instructor_dashboard.html | 6 + 9 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py create mode 100644 lms/djangoapps/instructor/management/commands/compute_grades.py create mode 100644 lms/djangoapps/instructor/offline_gradecalc.py diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index ec3b708ca7..64fe844801 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -12,6 +12,8 @@ admin.site.register(UserTestGroup) admin.site.register(CourseEnrollment) +admin.site.register(CourseEnrollmentAllowed) + admin.site.register(Registration) admin.site.register(PendingNameChange) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 0d4a37eda5..b58f8d5470 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,8 +5,6 @@ like DISABLE_START_DATES""" import logging import time -import student.models - from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -15,6 +13,13 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor +# student.models imports Role, which imports courseware.access ; use a try, to break the circular import +try: + from student.models import CourseEnrollmentAllowed +except Exception as err: + CourseEnrollmentAllowed = None + + DEBUG_ACCESS = False log = logging.getLogger(__name__) @@ -127,8 +132,9 @@ def _has_access_course_desc(user, course, action): return True # if user is in CourseEnrollmentAllowed with right course_id then can also enroll - if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): - return True + if user is not None and CourseEnrollmentAllowed: + if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py index cda4fbb788..f7e54d1800 100644 --- a/lms/djangoapps/courseware/admin.py +++ b/lms/djangoapps/courseware/admin.py @@ -7,3 +7,8 @@ from django.contrib import admin from django.contrib.auth.models import User admin.site.register(StudentModule) + +admin.site.register(OfflineComputedGrade) + +admin.site.register(OfflineComputedGradeLog) + diff --git a/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py new file mode 100644 index 0000000000..674f97cec8 --- /dev/null +++ b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'OfflineComputedGrade' + db.create_table('courseware_offlinecomputedgrade', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + ('gradeset', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('courseware', ['OfflineComputedGrade']) + + # Adding unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.create_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Adding model 'OfflineComputedGradeLog' + db.create_table('courseware_offlinecomputedgradelog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('seconds', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('nstudents', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('courseware', ['OfflineComputedGradeLog']) + + + def backwards(self, orm): + # Removing unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.delete_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Deleting model 'OfflineComputedGrade' + db.delete_table('courseware_offlinecomputedgrade') + + # Deleting model 'OfflineComputedGradeLog' + db.delete_table('courseware_offlinecomputedgradelog') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'courseware.offlinecomputedgrade': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'courseware.offlinecomputedgradelog': { + 'Meta': {'object_name': 'OfflineComputedGradeLog'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'courseware.studentmodule': { + 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}), + 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), + 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}), + 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['courseware'] \ No newline at end of file diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index ffc7c929de..21ef8b3d66 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -177,3 +177,40 @@ class StudentModuleCache(object): def append(self, student_module): self.cache.append(student_module) + + +class OfflineComputedGrade(models.Model): + """ + Table of grades computed offline for a given user and course. + """ + user = models.ForeignKey(User, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + updated = models.DateTimeField(auto_now=True, db_index=True) + + gradeset = models.TextField(null=True, blank=True) # grades, stored as JSON + + class Meta: + unique_together = (('user', 'course_id'), ) + + def __unicode__(self): + return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset) + + +class OfflineComputedGradeLog(models.Model): + """ + Log of when offline grades are computed. + Use this to be able to show instructor when the last computed grades were done. + """ + class Meta: + ordering = ["-created"] + get_latest_by = "created" + + course_id = models.CharField(max_length=255, db_index=True) + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + seconds = models.IntegerField(default=0) # seconds elapsed for computation + nstudents = models.IntegerField(default=0) + + def __unicode__(self): + return "[OCGLog] %s: %s" % (self.course_id, self.created) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py new file mode 100644 index 0000000000..717bfd5802 --- /dev/null +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# +# django management command: dump grades to csv files +# for use by batch processes + +import os, sys, string +import datetime +import json + +#import student.models +from instructor.offline_gradecalc import * +from courseware.courses import get_course_by_id +from xmodule.modulestore.django import modulestore + +from django.conf import settings +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Compute grades for all students in a course, and store result in DB.\n" + help += "Usage: compute_grades course_id_or_dir \n" + help += " course_id_or_dir: either course_id or course_dir\n" + + def handle(self, *args, **options): + + print "args = ", args + + course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' + + if len(args)>0: + course_id = args[0] + + try: + course = get_course_by_id(course_id) + except Exception as err: + if course_id in modulestore().courses: + course = modulestore().courses[course_id] + else: + print "-----------------------------------------------------------------------------" + print "Sorry, cannot find course %s" % course_id + print "Please provide a course ID or course data directory name, eg content-mit-801rq" + return + + print "-----------------------------------------------------------------------------" + print "Computing grades for %s" % (course.id) + + offline_grade_calculation(course.id) + + + + diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py new file mode 100644 index 0000000000..7c102805b4 --- /dev/null +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -0,0 +1,103 @@ +# ======== Offline calculation of grades ============================================================================= +# +# Computing grades of a large number of students can take a long time. These routines allow grades to +# be computed offline, by a batch process (eg cronjob). +# +# The grades are stored in the OfflineComputedGrade table of the courseware model. + +import json +import logging +import time + +import courseware.models + +from collections import namedtuple +from json import JSONEncoder +from courseware import grades, models +from courseware.courses import get_course_by_id +from django.contrib.auth.models import User, Group + + +class MyEncoder(JSONEncoder): + + def _iterencode(self, obj, markers=None): + if isinstance(obj, tuple) and hasattr(obj, '_asdict'): + gen = self._iterencode_dict(obj._asdict(), markers) + else: + gen = JSONEncoder._iterencode(self, obj, markers) + for chunk in gen: + yield chunk + + +def offline_grade_calculation(course_id): + ''' + Compute grades for all students for a specified course, and save results to the DB. + ''' + + tstart = time.time() + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') + + enc = MyEncoder() + + class DummyRequest(object): + META = {} + def __init__(self): + return + def get_host(self): + return 'edx.mit.edu' + def is_secure(self): + return False + + request = DummyRequest() + + print "%d enrolled students" % len(enrolled_students) + course = get_course_by_id(course_id) + + for student in enrolled_students: + gradeset = grades.grade(student, request, course, keep_raw_scores=True) + gs = enc.encode(gradeset) + ocg, created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_id) + ocg.gradeset = gs + ocg.save() + print "%s done" % student # print statement used because this is run by a management command + + tend = time.time() + dt = tend - tstart + + ocgl = models.OfflineComputedGradeLog(course_id=course_id, seconds=dt, nstudents=len(enrolled_students)) + ocgl.save() + print ocgl + print "All Done!" + + +def offline_grades_available(course_id): + ''' + Returns False if no offline grades available for specified course. + Otherwise returns latest log field entry about the available pre-computed grades. + ''' + ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_id) + if not ocgl: + return False + return ocgl.latest('created') + + +def student_grades(student, request, course, keep_raw_scores=False, use_offline=False): + ''' + This is the main interface to get grades. It has the same parameters as grades.grade, as well + as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB. + ''' + + if not use_offline: + return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores) + + try: + ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id) + except models.OfflineComputedGrade.DoesNotExist: + return dict(raw_scores=[], section_breakdown=[], + msg='Error: no offline gradeset available for %s, %s' % (student, course.id)) + + return json.loads(ocg.gradeset) + + + + diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0ea3cf0435..2e8db884ff 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,8 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views - +from .grading import StaffGrading +from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) @@ -103,6 +104,7 @@ def instructor_dashboard(request, course_id): # process actions from form POST action = request.POST.get('action', '') + use_offline = request.POST.get('use_offline_grades',False) if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: @@ -134,32 +136,32 @@ def instructor_dashboard(request, course_id): if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'list-students', {}, page='idashboard') elif 'Dump Grades' in action: log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades', {}, page='idashboard') elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, - get_raw_scores=True) + get_raw_scores=True, use_offline=use_offline) datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard') elif 'Download CSV of all student grades' in action: track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard') return return_csv('grades_{0}.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id)) + get_student_grade_summary_data(request, course, course_id, use_offline=use_offline)) elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard') return return_csv('grades_{0}_raw.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id, get_raw_scores=True)) + get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') @@ -174,7 +176,7 @@ def instructor_dashboard(request, course_id): elif action=='List assignments available for this course': log.debug(action) - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': ['Assignment Name']} @@ -184,7 +186,7 @@ def instructor_dashboard(request, course_id): msg += 'assignments=
%s
' % assignments elif action=='List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] @@ -202,7 +204,7 @@ def instructor_dashboard(request, course_id): if not aname: msg += "Please enter an assignment name" else: - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "Invalid assignment name '%s'" % aname else: @@ -401,6 +403,12 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) + #---------------------------------------- + # offline grades? + + if use_offline: + msg += "
Grades from %s" % offline_grades_available(course_id) + #---------------------------------------- # context for rendering @@ -416,7 +424,8 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), - 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING',''), + 'offline_grade_log' : offline_grades_available(course_id), } return render_to_response('courseware/instructor_dashboard.html', context) @@ -539,7 +548,7 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): return msg -def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False): +def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False): ''' Return data arrays with student identity and grades for specified course. @@ -563,7 +572,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header - gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: assignments += [score.section for score in gradeset['raw_scores']] @@ -582,20 +591,22 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, datarow.append('') if get_grades: - gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - student_grades = [score.earned for score in gradeset['raw_scores']] + # TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned'] + sgrades = [(getattr(score,'earned','') or score[0]) for score in gradeset['raw_scores']] else: - student_grades = [x['percent'] for x in gradeset['section_breakdown']] - datarow += student_grades - student.grades = student_grades # store in student object + sgrades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += sgrades + student.grades = sgrades # store in student object data.append(datarow) datatable['data'] = data return datatable - +#----------------------------------------------------------------------------- +# Staff grading @@ -616,7 +627,7 @@ def gradebook(request, course_id): student_info = [{'username': student.username, 'id': student.id, 'email': student.email, - 'grade_summary': grades.grade(student, request, course), + 'grade_summary': student_grades(student, request, course), 'realname': student.profile.name, } for student in enrolled_students] @@ -639,6 +650,10 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +#----------------------------------------------------------------------------- +# enrollment + + def _do_enroll_students(course, course_id, students, overload=False): """Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns""" @@ -731,6 +746,9 @@ def enroll_students(request, course_id): 'debug': new_students}) +#----------------------------------------------------------------------------- +# answer distribution + def get_answers_distribution(request, course_id): """ Get the distribution of answers for all graded problems in the course. diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index b2ec220484..235505fc29 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -71,6 +71,12 @@ function goto( mode) ##----------------------------------------------------------------------------- %if modeflag.get('Grades'): + + %if offline_grade_log: +

Pre-computed grades ${offline_grade_log} available: Use? +

+ %endif +

Gradebook

From 16b91cf73277e900ad746deba1ba25bd0a027f34 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:35:55 +0000 Subject: [PATCH 195/329] remove spurious old migration file --- .../migrations/0021_remove_askbot.py.old | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old deleted file mode 100644 index 89f7208f40..0000000000 --- a/common/djangoapps/student/migrations/0021_remove_askbot.py.old +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -ASKBOT_AUTH_USER_COLUMNS = ( - 'website', - 'about', - 'gold', - 'email_isvalid', - 'real_name', - 'location', - 'reputation', - 'gravatar', - 'bronze', - 'last_seen', - 'silver', - 'questions_per_page', - 'new_response_count', - 'seen_response_count', -) - - -class Migration(SchemaMigration): - - def forwards(self, orm): - "Kill the askbot" - # For MySQL, we're batching the alters together for performance reasons - if db.backend_name == 'mysql': - drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] - statement = "alter table `auth_user` {0};".format(", ".join(drops)) - db.execute(statement) - else: - for column in ASKBOT_AUTH_USER_COLUMNS: - db.delete_column('auth_user', column) - - def backwards(self, orm): - raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'student.courseenrollment': { - 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'student.pendingemailchange': { - 'Meta': {'object_name': 'PendingEmailChange'}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.pendingnamechange': { - 'Meta': {'object_name': 'PendingNameChange'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.registration': { - 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.testcenteruser': { - 'Meta': {'object_name': 'TestCenterUser'}, - 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), - 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), - 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), - 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), - 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), - 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), - 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), - 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), - 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) - }, - 'student.userprofile': { - 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, - 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), - 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), - 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) - }, - 'student.usertestgroup': { - 'Meta': {'object_name': 'UserTestGroup'}, - 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) - } - } - - complete_apps = ['student'] From e5c958082bff84c79bde4e979828796813335d72 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 5 Jan 2013 21:08:35 +0000 Subject: [PATCH 196/329] list/add/delete instructors on instructor dashboard --- lms/djangoapps/courseware/access.py | 14 ++++-- lms/djangoapps/instructor/views.py | 50 +++++++++++++++++-- .../courseware/instructor_dashboard.html | 10 ++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index b58f8d5470..60fcbff3a3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -171,13 +171,19 @@ def _has_access_course_desc(user, course, action): return _dispatch(checkers, action, user, course) + def _get_access_group_name_course_desc(course, action): ''' - Return name of group which gives staff access to course. Only understands action = 'staff' + Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor' ''' - if not action=='staff': - return [] - return _course_staff_group_name(course.location) + if action=='staff': + return _course_staff_group_name(course.location) + elif action=='instructor': + return _course_instructor_group_name(course.location) + + return [] + + def _has_access_error_desc(user, descriptor, action): """ diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8db884ff..2e8b9fd6bc 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -94,11 +94,17 @@ def instructor_dashboard(request, course_id): return response def get_staff_group(course): - staffgrp = get_access_group_name(course, 'staff') + return get_group(course, 'staff') + + def get_instructor_group(course): + return get_group(course, 'instructor') + + def get_group(course, groupname): + grpname = get_access_group_name(course, groupname) try: - group = Group.objects.get(name=staffgrp) + group = Group.objects.get(name=grpname) except Group.DoesNotExist: - group = Group(name=staffgrp) # create the group + group = Group(name=grpname) # create the group group.save() return group @@ -239,6 +245,16 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') + elif 'List course instructors' in action: + group = get_instructor_group(course) + msg += 'Instructor group = {0}'.format(group.name) + log.debug('instructor grp={0}'.format(group.name)) + uset = group.user_set.all() + datatable = {'header': ['Username', 'Full name']} + datatable['data'] = [[x.username, x.profile.name] for x in uset] + datatable['title'] = 'List of Instructors in course {0}'.format(course_id) + track.views.server_track(request, 'list-instructors', {}, page='idashboard') + elif action == 'Add course staff': uname = request.POST['staffuser'] try: @@ -253,6 +269,20 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Add instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Added {0} to instructor group = {1}'.format(user, group.name) + log.debug('staffgrp={0}'.format(group.name)) + user.groups.add(group) + track.views.server_track(request, 'add-instructor {0}'.format(user), {}, page='idashboard') + elif action == 'Remove course staff': uname = request.POST['staffuser'] try: @@ -267,6 +297,20 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Remove instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) + log.debug('instructorgrp={0}'.format(group.name)) + user.groups.remove(group) + track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') + #---------------------------------------- # forum administration diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 235505fc29..7f1912cd45 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -173,6 +173,16 @@ function goto( mode)
%endif + %if admin_access: +
+

+ +

+ + +


+ %endif + %if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:

From ec94f7328ce77d61329c0b477da52ae8a5037893 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:13:05 -0500 Subject: [PATCH 197/329] remove try...except workaround for circular import bug fixed by https://github.com/MITx/mitx/issues/1260 --- lms/djangoapps/courseware/access.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 60fcbff3a3..26f9fcdfd3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -13,12 +13,7 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor -# student.models imports Role, which imports courseware.access ; use a try, to break the circular import -try: - from student.models import CourseEnrollmentAllowed -except Exception as err: - CourseEnrollmentAllowed = None - +from student.models import CourseEnrollmentAllowed DEBUG_ACCESS = False From 9853c6323f5c3f7d6b65dce6126e647275d56e8b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 10 Jan 2013 15:19:38 -0500 Subject: [PATCH 198/329] Remove circular dependencies that connect student.models and django_comment_client Includes removal of "from django_comment_client.models import Role" from common/djangoapps/student/models.py Conflicts: common/djangoapps/student/models.py --- common/djangoapps/student/models.py | 11 ----------- lms/djangoapps/django_comment_client/models.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index d3254532bc..d9ce790ebe 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -49,7 +49,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver import comment_client as cc -from django_comment_client.models import Role log = logging.getLogger(__name__) @@ -280,16 +279,6 @@ class CourseEnrollmentAllowed(models.Model): return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) -@receiver(post_save, sender=CourseEnrollment) -def assign_default_role(sender, instance, **kwargs): - if instance.user.is_staff: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] - else: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] - - logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) - instance.user.roles.add(role) - #cache_relation(User.profile) #### Helper methods for use from python manage.py shell. diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 628ac21a4a..a6a2c23603 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -2,6 +2,10 @@ import logging from django.db import models from django.contrib.auth.models import User +from django.dispatch import receiver +from django.db.models.signals import post_save + +from student.models import CourseEnrollment from courseware.courses import get_course_by_id @@ -45,3 +49,14 @@ class Permission(models.Model): def __unicode__(self): return self.name + + +@receiver(post_save, sender=CourseEnrollment) +def assign_default_role(sender, instance, **kwargs): + if instance.user.is_staff: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] + else: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] + + logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) + instance.user.roles.add(role) From 5cc88ec1adbc04b4a6459c8acd90209bb2e5adc9 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:25:29 -0500 Subject: [PATCH 199/329] example course_id for compute_grades management command --- .../instructor/management/commands/compute_grades.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 717bfd5802..462833ba3c 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -19,15 +19,17 @@ class Command(BaseCommand): help = "Compute grades for all students in a course, and store result in DB.\n" help += "Usage: compute_grades course_id_or_dir \n" help += " course_id_or_dir: either course_id or course_dir\n" + help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' def handle(self, *args, **options): print "args = ", args - course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' - if len(args)>0: course_id = args[0] + else: + print self.help + return try: course = get_course_by_id(course_id) From 37f848949d80a0dc8aea45e6d8f7f560bbb61628 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:29:52 -0500 Subject: [PATCH 200/329] only is_staff users can add/edit/delete course instructors --- lms/djangoapps/instructor/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8b9fd6bc..07dbfacc64 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -245,7 +245,7 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') - elif 'List course instructors' in action: + elif 'List course instructors' in action and request.user.is_staff: group = get_instructor_group(course) msg += 'Instructor group = {0}'.format(group.name) log.debug('instructor grp={0}'.format(group.name)) @@ -269,7 +269,7 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Add instructor': + elif action == 'Add instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) @@ -297,7 +297,7 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Remove instructor': + elif action == 'Remove instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) From ed96046ad77e609330968834eeee8e4e6a573037 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:59:20 -0500 Subject: [PATCH 201/329] add documentation on remote gradebook xserver API --- doc/remote_gradebook.md | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 doc/remote_gradebook.md diff --git a/doc/remote_gradebook.md b/doc/remote_gradebook.md new file mode 100644 index 0000000000..3743e98753 --- /dev/null +++ b/doc/remote_gradebook.md @@ -0,0 +1,47 @@ +Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers". + +1. Definitions + +An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages. + +"Stellar" is the MIT on-campus gradebook system. + +2. Setup + +The remote gradebook xserver should be specified in the lms.envs configuration using + + MITX_FEATURES[REMOTE_GRADEBOOK_URL] + +Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg: + + "remote_gradebook": { + "name" : "STELLAR:/project/mitxdemosite", + "section" : "r01" + }, + +3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields: + + - submit: get-assignments, get-membership, post-grades, or get-sections + - gradebook: name of gradebook + - user: username of staff person initiating the request (for logging) + - section: (optional) name of section + +The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard. + +The data is a list of dicts (associative arrays). Each dict should be key:value. + +## For submit=post-grades: + +A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment). + +## For submit=get-assignments + +data keys = "AssignmentName" + +## For submit=get-membership + +data keys = "email", "name", "section" + +## For submit=get-sections + +data keys = "SectionName" From b7ad39a0b96de070776933ed96690ff4d3f9b35b Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:09:46 -0500 Subject: [PATCH 202/329] instructor dashboard shouldn't import .grading anymore --- lms/djangoapps/instructor/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 07dbfacc64..3a3380407d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -from .grading import StaffGrading from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) From 4f869ad38258a8664344188c5e20fa8a328311e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:10:52 -0500 Subject: [PATCH 203/329] remove spurious comment --- lms/djangoapps/instructor/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 3a3380407d..2d58799efe 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -649,9 +649,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, return datatable #----------------------------------------------------------------------------- -# Staff grading - - @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): From bcbf65e2b9f1108452f68136d802796463b1f974 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 09:45:10 -0500 Subject: [PATCH 204/329] Code reformat, line length fix, change text names to variables --- .../xmodule/combined_open_ended_module.py | 282 +++++++++--------- .../lib/xmodule/xmodule/open_ended_module.py | 214 ++++++------- common/lib/xmodule/xmodule/openendedchild.py | 22 +- .../xmodule/xmodule/self_assessment_module.py | 18 +- 4 files changed, 275 insertions(+), 261 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 244346625a..358a3b6995 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -45,14 +45,14 @@ class CombinedOpenEndedModule(XModule): # states INITIAL = 'initial' ASSESSING = 'assessing' - INTERMEDIATE_DONE='intermediate_done' + INTERMEDIATE_DONE = 'intermediate_done' DONE = 'done' - TASK_TYPES=["self", "ml", "instructor", "peer"] + TASK_TYPES = ["self", "ml", "instructor", "peer"] js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'), - ]} + ]} js_module_name = "CombinedOpenEnded" css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} @@ -88,7 +88,8 @@ class CombinedOpenEndedModule(XModule): Enter essay here. This is the answer. - {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"} + {"grader_settings" : "ml_grading.conf", + "problem_id" : "6.002x/Welcome/OETest"} @@ -108,9 +109,9 @@ class CombinedOpenEndedModule(XModule): #Tells the system which xml definition to load self.current_task_number = instance_state.get('current_task_number', 0) #This loads the states of the individual children - self.task_states= instance_state.get('task_states', []) + self.task_states = instance_state.get('task_states', []) #Overall state of the combined open ended module - self.state = instance_state.get('state', 'initial') + self.state = instance_state.get('state', self.INITIAL) self.attempts = instance_state.get('attempts', 0) @@ -124,13 +125,13 @@ class CombinedOpenEndedModule(XModule): #Static data is passed to the child modules to render self.static_data = { - 'max_score' : self._max_score, - 'max_attempts' : self.max_attempts, - 'prompt' : definition['prompt'], - 'rubric' : definition['rubric'] + 'max_score': self._max_score, + 'max_attempts': self.max_attempts, + 'prompt': definition['prompt'], + 'rubric': definition['rubric'] } - self.task_xml=definition['task_xml'] + self.task_xml = definition['task_xml'] self.setup_next_task() def get_tag_name(self, xml): @@ -139,7 +140,7 @@ class CombinedOpenEndedModule(XModule): Input: XML string Output: The name of the root tag """ - tag=etree.fromstring(xml).tag + tag = etree.fromstring(xml).tag return tag def overwrite_state(self, current_task_state): @@ -149,15 +150,15 @@ class CombinedOpenEndedModule(XModule): Input: Task state json string Output: Task state json string """ - last_response_data=self.get_last_response(self.current_task_number-1) + last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] - loaded_task_state=json.loads(current_task_state) - if loaded_task_state['state']== self.INITIAL: - loaded_task_state['state']=self.ASSESSING + loaded_task_state = json.loads(current_task_state) + if loaded_task_state['state'] == self.INITIAL: + loaded_task_state['state'] = self.ASSESSING loaded_task_state['created'] = "True" - loaded_task_state['history'].append({'answer' : last_response}) - current_task_state=json.dumps(loaded_task_state) + loaded_task_state['history'].append({'answer': last_response}) + current_task_state = json.dumps(loaded_task_state) return current_task_state def child_modules(self): @@ -167,17 +168,17 @@ class CombinedOpenEndedModule(XModule): Input: None Output: A dictionary of dictionaries containing the descriptor functions and module functions """ - child_modules={ - 'openended' : open_ended_module.OpenEndedModule, - 'selfassessment' : self_assessment_module.SelfAssessmentModule, + child_modules = { + 'openended': open_ended_module.OpenEndedModule, + 'selfassessment': self_assessment_module.SelfAssessmentModule, } - child_descriptors={ - 'openended' : open_ended_module.OpenEndedDescriptor, - 'selfassessment' : self_assessment_module.SelfAssessmentDescriptor, + child_descriptors = { + 'openended': open_ended_module.OpenEndedDescriptor, + 'selfassessment': self_assessment_module.SelfAssessmentDescriptor, } - children={ - 'modules' : child_modules, - 'descriptors' : child_descriptors, + children = { + 'modules': child_modules, + 'descriptors': child_descriptors, } return children @@ -188,41 +189,47 @@ class CombinedOpenEndedModule(XModule): Input: A boolean indicating whether or not the reset function is calling. Output: Boolean True (not useful right now) """ - current_task_state=None - if len(self.task_states)>self.current_task_number: - current_task_state=self.task_states[self.current_task_number] + current_task_state = None + if len(self.task_states) > self.current_task_number: + current_task_state = self.task_states[self.current_task_number] - self.current_task_xml=self.task_xml[self.current_task_number] + self.current_task_xml = self.task_xml[self.current_task_number] - if self.current_task_number>0: - self.allow_reset=self.check_allow_reset() + if self.current_task_number > 0: + self.allow_reset = self.check_allow_reset() if self.allow_reset: - self.current_task_number=self.current_task_number-1 + self.current_task_number = self.current_task_number - 1 - current_task_type=self.get_tag_name(self.current_task_xml) + current_task_type = self.get_tag_name(self.current_task_xml) - children=self.child_modules() + children = self.child_modules() - self.current_task_descriptor=children['descriptors'][current_task_type](self.system) - etree_xml=etree.fromstring(self.current_task_xml) + self.current_task_descriptor = children['descriptors'][current_task_type](self.system) + etree_xml = etree.fromstring(self.current_task_xml) - self.current_task_parsed_xml=self.current_task_descriptor.definition_from_xml(etree_xml,self.system) - if current_task_state is None and self.current_task_number==0: - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) + self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system) + if current_task_state is None and self.current_task_number == 0: + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING - elif current_task_state is None and self.current_task_number>0: - last_response_data =self.get_last_response(self.current_task_number-1) + self.state = self.ASSESSING + elif current_task_state is None and self.current_task_number > 0: + last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] - current_task_state = ('{"state": "assessing", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + current_task_state = ( + '{"state": "' + str(self.ASSESSING) + '", "version": 1, "max_score": ' + str(self._max_score) + ', ' + + '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, + instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) - self.state=self.ASSESSING + self.state = self.ASSESSING else: - if self.current_task_number>0 and not reset: - current_task_state=self.overwrite_state(current_task_state) - self.current_task=children['modules'][current_task_type](self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) + if self.current_task_number > 0 and not reset: + current_task_state = self.overwrite_state(current_task_state) + self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, + instance_state=current_task_state) log.debug(current_task_state) return True @@ -235,13 +242,14 @@ class CombinedOpenEndedModule(XModule): Output: the allow_reset attribute of the current module. """ if not self.allow_reset: - if self.current_task_number>0: - last_response_data=self.get_last_response(self.current_task_number-1) - current_response_data=self.get_current_attributes(self.current_task_number) + if self.current_task_number > 0: + last_response_data = self.get_last_response(self.current_task_number - 1) + current_response_data = self.get_current_attributes(self.current_task_number) - if current_response_data['min_score_to_attempt']>last_response_data['score'] or current_response_data['max_score_to_attempt'] last_response_data['score'] + or current_response_data['max_score_to_attempt'] < last_response_data['score']): + self.state = self.DONE + self.allow_reset = True return self.allow_reset @@ -251,18 +259,18 @@ class CombinedOpenEndedModule(XModule): Input: None Output: A dictionary that can be rendered into the combined open ended template. """ - task_html=self.get_html_base() + task_html = self.get_html_base() #set context variables and render template context = { - 'items': [{'content' : task_html}], + 'items': [{'content': task_html}], 'ajax_url': self.system.ajax_url, 'allow_reset': self.allow_reset, - 'state' : self.state, - 'task_count' : len(self.task_xml), - 'task_number' : self.current_task_number+1, - 'status' : self.get_status(), - } + 'state': self.state, + 'task_count': len(self.task_xml), + 'task_number': self.current_task_number + 1, + 'status': self.get_status(), + } return context @@ -272,7 +280,7 @@ class CombinedOpenEndedModule(XModule): Input: None Output: rendered html """ - context=self.get_context() + context = self.get_context() html = self.system.render_template('combined_open_ended.html', context) return html @@ -283,7 +291,7 @@ class CombinedOpenEndedModule(XModule): Input: None Output: HTML rendered directly via Mako """ - context=self.get_context() + context = self.get_context() html = render_to_string('combined_open_ended.html', context) return html @@ -304,11 +312,11 @@ class CombinedOpenEndedModule(XModule): Input: The number of the task. Output: The minimum and maximum scores needed to move on to the specified task. """ - task_xml=self.task_xml[task_number] - etree_xml=etree.fromstring(task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('max_score_to_attempt',self._max_score)) - return {'min_score_to_attempt' : min_score_to_attempt, 'max_score_to_attempt' : max_score_to_attempt} + task_xml = self.task_xml[task_number] + etree_xml = etree.fromstring(task_xml) + min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) + max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) + return {'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt} def get_last_response(self, task_number): """ @@ -316,49 +324,50 @@ class CombinedOpenEndedModule(XModule): Input: The number of the task. Output: A dictionary that contains information about the specified task. """ - last_response="" + last_response = "" task_state = self.task_states[task_number] - task_xml=self.task_xml[task_number] - task_type=self.get_tag_name(task_xml) + task_xml = self.task_xml[task_number] + task_type = self.get_tag_name(task_xml) - children=self.child_modules() + children = self.child_modules() - task_descriptor=children['descriptors'][task_type](self.system) - etree_xml=etree.fromstring(task_xml) + task_descriptor = children['descriptors'][task_type](self.system) + etree_xml = etree.fromstring(task_xml) - min_score_to_attempt=int(etree_xml.attrib.get('min_score_to_attempt',0)) - max_score_to_attempt=int(etree_xml.attrib.get('max_score_to_attempt',self._max_score)) + min_score_to_attempt = int(etree_xml.attrib.get('min_score_to_attempt', 0)) + max_score_to_attempt = int(etree_xml.attrib.get('max_score_to_attempt', self._max_score)) - task_parsed_xml=task_descriptor.definition_from_xml(etree_xml,self.system) - task=children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, self.static_data, instance_state=task_state) - last_response=task.latest_answer() + task_parsed_xml = task_descriptor.definition_from_xml(etree_xml, self.system) + task = children['modules'][task_type](self.system, self.location, task_parsed_xml, task_descriptor, + self.static_data, instance_state=task_state) + last_response = task.latest_answer() last_score = task.latest_score() last_post_assessment = task.latest_post_assessment() - last_post_feedback="" - if task_type=="openended": + last_post_feedback = "" + if task_type == "openended": last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False) - if isinstance(last_post_assessment,list): - eval_list=[] - for i in xrange(0,len(last_post_assessment)): + if isinstance(last_post_assessment, list): + eval_list = [] + for i in xrange(0, len(last_post_assessment)): eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i])) - last_post_evaluation="".join(eval_list) + last_post_evaluation = "".join(eval_list) else: last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment) last_post_assessment = last_post_evaluation last_correctness = task.is_last_response_correct() max_score = task.max_score() state = task.state - last_response_dict={ - 'response' : last_response, - 'score' : last_score, - 'post_assessment' : last_post_assessment, - 'type' : task_type, - 'max_score' : max_score, - 'state' : state, - 'human_state' : task.HUMAN_NAMES[state], - 'correct' : last_correctness, - 'min_score_to_attempt' : min_score_to_attempt, - 'max_score_to_attempt' : max_score_to_attempt, + last_response_dict = { + 'response': last_response, + 'score': last_score, + 'post_assessment': last_post_assessment, + 'type': task_type, + 'max_score': max_score, + 'state': state, + 'human_state': task.HUMAN_NAMES[state], + 'correct': last_correctness, + 'min_score_to_attempt': min_score_to_attempt, + 'max_score_to_attempt': max_score_to_attempt, } return last_response_dict @@ -369,28 +378,28 @@ class CombinedOpenEndedModule(XModule): Input: None Output: boolean indicating whether or not the task state changed. """ - changed=False + changed = False if not self.allow_reset: self.task_states[self.current_task_number] = self.current_task.get_instance_state() - current_task_state=json.loads(self.task_states[self.current_task_number]) - if current_task_state['state']==self.DONE: - self.current_task_number+=1 - if self.current_task_number>=(len(self.task_xml)): - self.state=self.DONE - self.current_task_number=len(self.task_xml)-1 + current_task_state = json.loads(self.task_states[self.current_task_number]) + if current_task_state['state'] == self.DONE: + self.current_task_number += 1 + if self.current_task_number >= (len(self.task_xml)): + self.state = self.DONE + self.current_task_number = len(self.task_xml) - 1 else: - self.state=self.INITIAL - changed=True + self.state = self.INITIAL + changed = True self.setup_next_task() return changed - def update_task_states_ajax(self,return_html): + def update_task_states_ajax(self, return_html): """ Runs the update task states function for ajax calls. Currently the same as update_task_states Input: The html returned by the handle_ajax function of the child Output: New html that should be rendered """ - changed=self.update_task_states() + changed = self.update_task_states() if changed: #return_html=self.get_html() pass @@ -402,12 +411,12 @@ class CombinedOpenEndedModule(XModule): Input: AJAX get dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ - task_number=int(get['task_number']) + task_number = int(get['task_number']) self.update_task_states() - response_dict=self.get_last_response(task_number) - context = {'results' : response_dict['post_assessment'], 'task_number' : task_number+1} + response_dict = self.get_last_response(task_number) + context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1} html = render_to_string('combined_open_ended_results.html', context) - return {'html' : html, 'success' : True} + return {'html': html, 'success': True} def handle_ajax(self, dispatch, get): """ @@ -423,15 +432,15 @@ class CombinedOpenEndedModule(XModule): handlers = { 'next_problem': self.next_problem, 'reset': self.reset, - 'get_results' : self.get_results - } + 'get_results': self.get_results + } if dispatch not in handlers: - return_html = self.current_task.handle_ajax(dispatch,get, self.system) + return_html = self.current_task.handle_ajax(dispatch, get, self.system) return self.update_task_states_ajax(return_html) d = handlers[dispatch](get) - return json.dumps(d,cls=ComplexEncoder) + return json.dumps(d, cls=ComplexEncoder) def next_problem(self, get): """ @@ -440,7 +449,7 @@ class CombinedOpenEndedModule(XModule): Output: Dictionary to be rendered """ self.update_task_states() - return {'success' : True, 'html' : self.get_html_nonsystem(), 'allow_reset' : self.allow_reset} + return {'success': True, 'html': self.get_html_nonsystem(), 'allow_reset': self.allow_reset} def reset(self, get): """ @@ -457,17 +466,17 @@ class CombinedOpenEndedModule(XModule): 'success': False, 'error': 'Too many attempts.' } - self.state=self.INITIAL - self.allow_reset=False - for i in xrange(0,len(self.task_xml)): - self.current_task_number=i + self.state = self.INITIAL + self.allow_reset = False + for i in xrange(0, len(self.task_xml)): + self.current_task_number = i self.setup_next_task(reset=True) self.current_task.reset(self.system) - self.task_states[self.current_task_number]=self.current_task.get_instance_state() - self.current_task_number=0 - self.allow_reset=False + self.task_states[self.current_task_number] = self.current_task.get_instance_state() + self.current_task_number = 0 + self.allow_reset = False self.setup_next_task() - return {'success': True, 'html' : self.get_html_nonsystem()} + return {'success': True, 'html': self.get_html_nonsystem()} def get_instance_state(self): """ @@ -482,8 +491,8 @@ class CombinedOpenEndedModule(XModule): 'state': self.state, 'task_states': self.task_states, 'attempts': self.attempts, - 'ready_to_reset' : self.allow_reset, - } + 'ready_to_reset': self.allow_reset, + } return json.dumps(state) @@ -493,16 +502,17 @@ class CombinedOpenEndedModule(XModule): Input: None Output: The status html to be rendered """ - status=[] - for i in xrange(0,self.current_task_number+1): + status = [] + for i in xrange(0, self.current_task_number + 1): task_data = self.get_last_response(i) - task_data.update({'task_number' : i+1}) + task_data.update({'task_number': i + 1}) status.append(task_data) - context = {'status_list' : status} + context = {'status_list': status} status_html = self.system.render_template("combined_open_ended_status.html", context) return status_html + class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding combined open ended questions @@ -532,18 +542,18 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ expected_children = ['task', 'rubric', 'prompt'] for child in expected_children: - if len(xml_object.xpath(child)) == 0 : + if len(xml_object.xpath(child)) == 0: raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) def parse_task(k): """Assumes that xml_object has child k""" - return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0,len(xml_object.xpath(k)))] + return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))] def parse(k): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'task_xml': parse_task('task'), 'prompt' : parse('prompt'), 'rubric' : parse('rubric')} + return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')} def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 5649cbbd2c..45d6501816 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -49,6 +49,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """ + def setup_response(self, system, location, definition, descriptor): """ Sets up the response type. @@ -65,8 +66,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE) #This is needed to attach feedback to specific responses later - self.submission_id=None - self.grader_id=None + self.submission_id = None + self.grader_id = None if oeparam is None: raise ValueError("No oeparam found in problem xml.") @@ -77,10 +78,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self._parse(oeparam, self.prompt, self.rubric, system) - if self.created=="True" and self.state == self.ASSESSING: - self.created="False" + if self.created == "True" and self.state == self.ASSESSING: + self.created = "False" self.send_to_grader(self.latest_answer(), system) - self.created="False" + self.created = "False" def _parse(self, oeparam, prompt, rubric, system): ''' @@ -94,8 +95,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload prompt_string = stringify_children(prompt) rubric_string = stringify_children(rubric) - self.prompt=prompt_string - self.rubric=rubric_string + self.prompt = prompt_string + self.rubric = rubric_string grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -113,13 +114,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') parsed_grader_payload.update({ - 'location' : system.location.url(), - 'course_id' : system.course_id, - 'prompt' : prompt_string, - 'rubric' : rubric_string, - 'initial_display' : self.initial_display, - 'answer' : self.answer, - }) + 'location': system.location.url(), + 'course_id': system.course_id, + 'prompt': prompt_string, + 'rubric': rubric_string, + 'initial_display': self.initial_display, + 'answer': self.answer, + }) updated_grader_payload = json.dumps(parsed_grader_payload) self.payload = {'grader_payload': updated_grader_payload} @@ -131,10 +132,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @param system: ModuleSystem @return: Success indicator """ - self.state=self.DONE - return {'success' : True} + self.state = self.DONE + return {'success': True} - def message_post(self,get, system): + def message_post(self, get, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) Returns a boolean success/fail and an error message @@ -143,22 +144,23 @@ class OpenEndedModule(openendedchild.OpenEndedChild): event_info = dict() event_info['problem_id'] = system.location.url() event_info['student_id'] = system.anonymous_student_id - event_info['survey_responses']= get + event_info['survey_responses'] = get - survey_responses=event_info['survey_responses'] + survey_responses = event_info['survey_responses'] for tag in ['feedback', 'submission_id', 'grader_id', 'score']: if tag not in survey_responses: - return {'success' : False, 'msg' : "Could not find needed tag {0}".format(tag)} + return {'success': False, 'msg': "Could not find needed tag {0}".format(tag)} try: - submission_id=int(survey_responses['submission_id']) + submission_id = int(survey_responses['submission_id']) grader_id = int(survey_responses['grader_id']) feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) score = int(survey_responses['score']) except: - error_message=("Could not parse submission id, grader id, " - "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses)) + error_message = ("Could not parse submission id, grader id, " + "or feedback from message_post ajax call. Here is the message data: {0}".format( + survey_responses)) log.exception(error_message) - return {'success' : False, 'msg' : "There was an error saving your feedback. Please contact course staff."} + return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."} qinterface = system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -175,26 +177,26 @@ class OpenEndedModule(openendedchild.OpenEndedChild): student_info = {'anonymous_student_id': anonymous_student_id, 'submission_time': qtime, - } - contents= { - 'feedback' : feedback, - 'submission_id' : submission_id, - 'grader_id' : grader_id, + } + contents = { + 'feedback': feedback, + 'submission_id': submission_id, + 'grader_id': grader_id, 'score': score, - 'student_info' : json.dumps(student_info), - } + 'student_info': json.dumps(student_info), + } (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) #Convert error to a success value - success=True + success = True if error: - success=False + success = False - self.state=self.DONE + self.state = self.DONE - return {'success' : success, 'msg' : "Successfully submitted your feedback."} + return {'success': success, 'msg': "Successfully submitted your feedback."} def send_to_grader(self, submission, system): """ @@ -226,14 +228,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # Metadata related to the student submission revealed to the external grader student_info = {'anonymous_student_id': anonymous_student_id, 'submission_time': qtime, - } + } #Update contents with student response and student info contents.update({ 'student_info': json.dumps(student_info), 'student_response': submission, - 'max_score' : self.max_score(), - }) + 'max_score': self.max_score(), + }) # Submit request. When successful, 'msg' is the prior length of the queue (error, msg) = qinterface.send_to_queue(header=xheader, @@ -241,7 +243,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): # State associated with the queueing request queuestate = {'key': queuekey, - 'time': qtime,} + 'time': qtime, } return True def _update_score(self, score_msg, queuekey, system): @@ -258,7 +260,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.record_latest_score(new_score_msg['score']) self.record_latest_post_assessment(score_msg) - self.state=self.POST_ASSESSMENT + self.state = self.POST_ASSESSMENT return True @@ -313,24 +315,24 @@ class OpenEndedModule(openendedchild.OpenEndedChild): """ return priorities.get(elt[0], default_priority) - def encode_values(feedback_type,value): - feedback_type=str(feedback_type).encode('ascii', 'ignore') - if not isinstance(value,basestring): - value=str(value) - value=value.encode('ascii', 'ignore') - return feedback_type,value + def encode_values(feedback_type, value): + feedback_type = str(feedback_type).encode('ascii', 'ignore') + if not isinstance(value, basestring): + value = str(value) + value = value.encode('ascii', 'ignore') + return feedback_type, value def format_feedback(feedback_type, value): - feedback_type,value=encode_values(feedback_type,value) - feedback= """ + feedback_type, value = encode_values(feedback_type, value) + feedback = """

{value}
""".format(feedback_type=feedback_type, value=value) return feedback - def format_feedback_hidden(feedback_type , value): - feedback_type,value=encode_values(feedback_type,value) + def format_feedback_hidden(feedback_type, value): + feedback_type, value = encode_values(feedback_type, value) feedback = """ """.format(feedback_type=feedback_type, value=value) @@ -360,11 +362,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): else: feedback_list_part1 = format_feedback('errors', response_items['feedback']) - feedback_list_part2=(u"\n".join([format_feedback_hidden(feedback_type,value) - for feedback_type,value in response_items.items() - if feedback_type in ['submission_id', 'grader_id']])) + feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value) + for feedback_type, value in response_items.items() + if feedback_type in ['submission_id', 'grader_id']])) - return u"\n".join([feedback_list_part1,feedback_list_part2]) + return u"\n".join([feedback_list_part1, feedback_list_part2]) def _format_feedback(self, response_items): """ @@ -378,13 +380,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not response_items['success']: return system.render_template("open_ended_error.html", - {'errors' : feedback}) + {'errors': feedback}) feedback_template = render_to_string("open_ended_feedback.html", { 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, - }) + }) return feedback_template @@ -403,57 +405,57 @@ class OpenEndedModule(openendedchild.OpenEndedChild): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) """ - fail = {'valid' : False, 'score' : 0, 'feedback' : ''} + fail = {'valid': False, 'score': 0, 'feedback': ''} try: score_result = json.loads(score_msg) except (TypeError, ValueError): - error_message=("External grader message should be a JSON-serialized dict." - " Received score_msg = {0}".format(score_msg)) + error_message = ("External grader message should be a JSON-serialized dict." + " Received score_msg = {0}".format(score_msg)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail if not isinstance(score_result, dict): - error_message=("External grader message should be a JSON-serialized dict." - " Received score_result = {0}".format(score_result)) + error_message = ("External grader message should be a JSON-serialized dict." + " Received score_result = {0}".format(score_result)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: if tag not in score_result: - error_message=("External grader message is missing required tag: {0}" - .format(tag)) + error_message = ("External grader message is missing required tag: {0}" + .format(tag)) log.error(error_message) - fail['feedback']=error_message + fail['feedback'] = error_message return fail - #This is to support peer grading + #This is to support peer grading if isinstance(score_result['score'], list): - feedback_items=[] - for i in xrange(0,len(score_result['score'])): - new_score_result={ - 'score' : score_result['score'][i], - 'feedback' : score_result['feedback'][i], - 'grader_type' : score_result['grader_type'], - 'success' : score_result['success'], - 'grader_id' : score_result['grader_id'][i], - 'submission_id' : score_result['submission_id'] - } + feedback_items = [] + for i in xrange(0, len(score_result['score'])): + new_score_result = { + 'score': score_result['score'][i], + 'feedback': score_result['feedback'][i], + 'grader_type': score_result['grader_type'], + 'success': score_result['success'], + 'grader_id': score_result['grader_id'][i], + 'submission_id': score_result['submission_id'] + } feedback_items.append(self._format_feedback(new_score_result)) if join_feedback: - feedback="".join(feedback_items) + feedback = "".join(feedback_items) else: - feedback=feedback_items + feedback = feedback_items score = int(median(score_result['score'])) else: #This is for instructor and ML grading feedback = self._format_feedback(score_result) - score=score_result['score'] + score = score_result['score'] - self.submission_id=score_result['submission_id'] - self.grader_id=score_result['grader_id'] + self.submission_id = score_result['submission_id'] + self.grader_id = score_result['grader_id'] - return {'valid' : True, 'score' : score, 'feedback' : feedback} + return {'valid': True, 'score': score, 'feedback': feedback} def latest_post_assessment(self, short_feedback=False, join_feedback=True): """ @@ -468,17 +470,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): if not short_feedback: return feedback_dict['feedback'] if feedback_dict['valid'] else '' if feedback_dict['valid']: - short_feedback = self._convert_longform_feedback_to_html(json.loads(self.history[-1].get('post_assessment', ""))) + short_feedback = self._convert_longform_feedback_to_html( + json.loads(self.history[-1].get('post_assessment', ""))) return short_feedback if feedback_dict['valid'] else '' - def format_feedback_with_evaluation(self,feedback): + def format_feedback_with_evaluation(self, feedback): """ Renders a given html feedback into an evaluation template @param feedback: HTML feedback @return: Rendered html """ - context={'msg' : feedback, 'id' : "1", 'rows' : 50, 'cols' : 50} - html= render_to_string('open_ended_evaluation.html', context) + context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50} + html = render_to_string('open_ended_evaluation.html', context) return html def handle_ajax(self, dispatch, get, system): @@ -494,10 +497,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): handlers = { 'save_answer': self.save_answer, 'score_update': self.update_score, - 'save_post_assessment' : self.message_post, - 'skip_post_assessment' : self.skip_post_assessment, - 'check_for_score' : self.check_for_score, - } + 'save_post_assessment': self.message_post, + 'skip_post_assessment': self.skip_post_assessment, + 'check_for_score': self.check_for_score, + } if dispatch not in handlers: return 'Error' @@ -508,7 +511,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): d.update({ 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), - }) + }) return json.dumps(d, cls=ComplexEncoder) def check_for_score(self, get, system): @@ -519,7 +522,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): @return: Returns the current state """ state = self.state - return {'state' : state} + return {'state': state} def save_answer(self, get, system): """ @@ -545,7 +548,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.send_to_grader(get['student_answer'], system) self.change_state(self.ASSESSING) - return {'success': True,} + return {'success': True, } def update_score(self, get, system): """ @@ -571,11 +574,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): latest = self.latest_answer() previous_answer = latest if latest is not None else self.initial_display post_assessment = self.latest_post_assessment() - score= self.latest_score() + score = self.latest_score() correct = 'correct' if self.is_submission_correct(score) else 'incorrect' else: - post_assessment="" - correct="" + post_assessment = "" + correct = "" previous_answer = self.initial_display context = { @@ -583,17 +586,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'previous_answer': previous_answer, 'state': self.state, 'allow_reset': self._allow_reset(), - 'rows' : 30, - 'cols' : 80, - 'id' : 'open_ended', - 'msg' : post_assessment, - 'child_type' : 'openended', - 'correct' : correct, - } + 'rows': 30, + 'cols': 80, + 'id': 'open_ended', + 'msg': post_assessment, + 'child_type': 'openended', + 'correct': correct, + } log.debug(context) html = system.render_template('open_ended.html', context) return html + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding open ended response questions to courses @@ -627,7 +631,7 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """Assumes that xml_object has child k""" return xml_object.xpath(k)[0] - return {'oeparam': parse('openendedparam'),} + return {'oeparam': parse('openendedparam'), } def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index ce1b15074f..aa83a35c9d 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -63,11 +63,11 @@ class OpenEndedChild(): DONE = 'done' #This is used to tell students where they are at in the module - HUMAN_NAMES={ - 'initial' : 'Started', - 'assessing' : 'Being scored', - 'post_assessment' : 'Scoring finished', - 'done' : 'Problem complete', + HUMAN_NAMES = { + 'initial': 'Started', + 'assessing': 'Being scored', + 'post_assessment': 'Scoring finished', + 'done': 'Problem complete', } def __init__(self, system, location, definition, descriptor, static_data, @@ -84,7 +84,7 @@ class OpenEndedChild(): # Scores are on scale from 0 to max_score self.history = instance_state.get('history', []) - self.state = instance_state.get('state', 'initial') + self.state = instance_state.get('state', self.INITIAL) self.created = instance_state.get('created', "False") @@ -171,8 +171,8 @@ class OpenEndedChild(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, - 'created' : "False", - } + 'created': "False", + } return json.dumps(state) def _allow_reset(self): @@ -244,8 +244,8 @@ class OpenEndedChild(): @param score: Numeric score. @return: Boolean correct. """ - correct=False - if(isinstance(score,(int, long, float, complex))): + correct = False + if(isinstance(score, (int, long, float, complex))): score_ratio = int(score) / float(self.max_score()) correct = (score_ratio >= 0.66) return correct @@ -255,7 +255,7 @@ class OpenEndedChild(): Checks to see if the last response in the module is correct. @return: 'correct' if correct, otherwise 'incorrect' """ - score=self.get_score()['score'] + score = self.get_score()['score'] correctness = 'correct' if self.is_submission_correct(score) else 'incorrect' return correctness diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 88632a38d0..870f3ea169 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -40,6 +40,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): """ + def setup_response(self, system, location, definition, descriptor): """ Sets up the module @@ -76,7 +77,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'initial_message': self.get_message_html(), 'state': self.state, 'allow_reset': self._allow_reset(), - 'child_type' : 'selfassessment', + 'child_type': 'selfassessment', } html = system.render_template('self_assessment_prompt.html', context) @@ -112,7 +113,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) - def get_rubric_html(self,system): + def get_rubric_html(self, system): """ Return the appropriate version of the rubric, based on the state. """ @@ -121,8 +122,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): # we'll render it context = {'rubric': self.rubric, - 'max_score' : self._max_score, - } + 'max_score': self._max_score, + } if self.state == self.ASSESSING: context['read_only'] = False @@ -133,7 +134,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return system.render_template('self_assessment_rubric.html', context) - def get_hint_html(self,system): + def get_hint_html(self, system): """ Return the appropriate version of the hint view, based on state. """ @@ -201,7 +202,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return { 'success': True, 'rubric_html': self.get_rubric_html(system) - } + } def save_assessment(self, get, system): """ @@ -228,7 +229,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): self.record_latest_score(score) - d = {'success': True,} + d = {'success': True, } if score == self.max_score(): self.change_state(self.DONE) @@ -264,7 +265,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'allow_reset': self._allow_reset()} - class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): """ Module for adding self assessment questions to courses @@ -302,7 +302,7 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): return {'submitmessage': parse('submitmessage'), 'hintprompt': parse('hintprompt'), - } + } def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' From 1dca370a7f9b0f328ebf1da826f1304aebd60696 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 09:50:43 -0500 Subject: [PATCH 205/329] Better initial documentation of combined open ended --- .../xmodule/xmodule/combined_open_ended_module.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 358a3b6995..17355e9ce2 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -39,6 +39,19 @@ class CombinedOpenEndedModule(XModule): """ This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). It transitions between problems, and support arbitrary ordering. + Each combined open ended module contains one or multiple "child" modules. + Child modules track their own state, and can transition between states. They also implement get_html and + handle_ajax. + The combined open ended module transitions between child modules as appropriate, tracks its own state, and passess + ajax requests from the browser to the child module or handles them itself (in the cases of reset and next problem) + ajax actions implemented by all children are: + 'save_answer' -- Saves the student answer + 'save_assessment' -- Saves the student assessment (or external grader assessment) + 'save_post_assessment' -- saves a post assessment (hint, feedback on feedback, etc) + ajax actions implemented by combined open ended module are: + 'reset' -- resets the whole combined open ended module and returns to the first child module + 'next_problem' -- moves to the next child module + 'get_results' -- gets results from a given child module """ STATE_VERSION = 1 @@ -163,7 +176,7 @@ class CombinedOpenEndedModule(XModule): def child_modules(self): """ - Returns the functions associated with the child modules in a dictionary. This makes writing functions + Returns the constructors associated with the child modules in a dictionary. This makes writing functions simpler (saves code duplication) Input: None Output: A dictionary of dictionaries containing the descriptor functions and module functions From 52164f58ffb0edf577343df6856c8488457aedae Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 11:39:32 -0500 Subject: [PATCH 206/329] Fix self.get call in peer grading service --- lms/djangoapps/open_ended_grading/grading_service.py | 1 + lms/djangoapps/open_ended_grading/peer_grading_service.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/open_ended_grading/grading_service.py b/lms/djangoapps/open_ended_grading/grading_service.py index 96bd931448..7362411daa 100644 --- a/lms/djangoapps/open_ended_grading/grading_service.py +++ b/lms/djangoapps/open_ended_grading/grading_service.py @@ -62,6 +62,7 @@ class GradingService(object): """ Make a get request to the grading controller """ + log.debug(params) op = lambda: self.session.get(url, allow_redirects=allow_redirects, params=params) diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py index 859499ff7e..9ef0383fb5 100644 --- a/lms/djangoapps/open_ended_grading/peer_grading_service.py +++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py @@ -81,7 +81,7 @@ class PeerGradingService(GradingService): self.get_problem_list_url = self.url + '/get_problem_list/' def get_next_submission(self, problem_location, grader_id): - response = self.get(self.get_next_submission_url, False, + response = self.get(self.get_next_submission_url, {'location': problem_location, 'grader_id': grader_id}) return response From 7febe2c2c811742d73d153cfd7afbe19025ca23d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 12:43:57 -0500 Subject: [PATCH 207/329] Integrate minimal rubric display --- .../css/combinedopenended/display.scss | 45 ++++++++ .../lib/xmodule/xmodule/open_ended_module.py | 109 +++++++++++++++++- lms/templates/open_ended_feedback.html | 1 + lms/templates/open_ended_rubric.html | 17 +++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 lms/templates/open_ended_rubric.html diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index b5eb4b52e6..75e87e1f07 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -576,4 +576,49 @@ section.open-ended-child { font-size: 0.9em; } + .rubric { + tr { + margin:10px 0px; + height: 100%; + } + td { + padding: 20px 0px; + margin: 10px 0px; + height: 100%; + } + th { + padding: 5px; + margin: 5px; + } + label, + .view-only { + margin:10px; + position: relative; + padding: 15px; + width: 200px; + height:100%; + display: inline-block; + min-height: 50px; + min-width: 50px; + background-color: #CCC; + font-size: 1em; + } + .grade { + position: absolute; + bottom:0px; + right:0px; + margin:10px; + } + .selected-grade { + background: #666; + color: white; + } + input[type=radio]:checked + label { + background: #666; + color: white; } + input[class='score-selection'] { + display: none; + } + } + } diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index 45d6501816..e4f74a9124 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -376,7 +376,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): Return error message or feedback template """ + log.debug(response_items) + rubric_feedback="" feedback = self._convert_longform_feedback_to_html(response_items) + if response_items['rubric_scores_complete']: + rubric_feedback = self.render_rubric(response_items['rubric_xml']) if not response_items['success']: return system.render_template("open_ended_error.html", @@ -386,6 +390,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'grader_type': response_items['grader_type'], 'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'feedback': feedback, + 'rubric_feedback' : rubric_feedback }) return feedback_template @@ -429,7 +434,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): log.error(error_message) fail['feedback'] = error_message return fail - #This is to support peer grading + #This is to support peer grading if isinstance(score_result['score'], list): feedback_items = [] for i in xrange(0, len(score_result['score'])): @@ -439,7 +444,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'grader_type': score_result['grader_type'], 'success': score_result['success'], 'grader_id': score_result['grader_id'][i], - 'submission_id': score_result['submission_id'] + 'submission_id': score_result['submission_id'], + 'rubric_scores_complete' : score_result['rubric_scores_complete'], + 'rubric_xml' : score_result['rubric_xml'], } feedback_items.append(self._format_feedback(new_score_result)) if join_feedback: @@ -597,6 +604,104 @@ class OpenEndedModule(openendedchild.OpenEndedChild): html = system.render_template('open_ended.html', context) return html + def render_rubric(self, rubric_xml): + rubric_categories = OpenEndedModule.extract_rubric_categories(rubric_xml) + html = render_to_string('open_ended_rubric.html', rubric_categories) + return html + + @staticmethod + def extract_rubric_categories(element): + ''' + Contstruct a list of categories such that the structure looks like: + [ { category: "Category 1 Name", + options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}] + }, + { category: "Category 2 Name", + options: [{text: "Option 1 Name", points: 0}, + {text: "Option 2 Name", points: 1}, + {text: "Option 3 Name", points: 2]}] + + ''' + element = etree.fromstring(element) + categories = [] + for category in element: + if category.tag != 'category': + raise Exception("[capa.inputtypes.extract_categories] Expected a tag: got {0} instead".format(category.tag)) + else: + categories.append(OpenEndedModule.extract_category(category)) + return categories + + @staticmethod + def extract_category(category): + ''' + construct an individual category + {category: "Category 1 Name", + options: [{text: "Option 1 text", points: 1}, + {text: "Option 2 text", points: 2}]} + + all sorting and auto-point generation occurs in this function + ''' + descriptionxml = category[0] + scorexml = category[1] + optionsxml = category[2:] + + # parse description + if descriptionxml.tag != 'description': + raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag)) + + if scorexml.tag != 'score': + raise Exception("[extract_category]: expected score tag, got {0} instead".format(descriptionxml.tag)) + + description = descriptionxml.text + score = int(scorexml.text) + + cur_points = 0 + options = [] + autonumbering = True + # parse options + for option in optionsxml: + if option.tag != 'option': + raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag)) + else: + pointstr = option.get("points") + if pointstr: + autonumbering = False + # try to parse this into an int + try: + points = int(pointstr) + except ValueError: + raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr)) + elif autonumbering: + # use the generated one if we're in the right mode + points = cur_points + cur_points = cur_points + 1 + else: + raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.") + optiontext = option.text + options.append({'text': option.text, 'points': points}) + + # sort and check for duplicates + options = sorted(options, key=lambda option: option['points']) + OpenEndedModule.validate_options(options) + + return {'description': description, 'options': options, 'score' : score} + + @staticmethod + def validate_options(options): + ''' + Validates a set of options. This can and should be extended to filter out other bad edge cases + ''' + if len(options) == 0: + raise Exception("[extract_category]: no options associated with this category") + if len(options) == 1: + return + prev = options[0]['points'] + for option in options[1:]: + if prev == option['points']: + raise Exception("[extract_category]: found duplicate point values between two different options") + else: + prev = option['points'] + class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html index cb90006456..d8aa3d1a9e 100644 --- a/lms/templates/open_ended_feedback.html +++ b/lms/templates/open_ended_feedback.html @@ -12,5 +12,6 @@
${ feedback | n}
+ ${rubric_feedback | n}
\ No newline at end of file diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html new file mode 100644 index 0000000000..f89cecee80 --- /dev/null +++ b/lms/templates/open_ended_rubric.html @@ -0,0 +1,17 @@ + + % for i in range(len(categories)): + <% category = categories[i] %> + + + % for j in range(len(category['options'])): + <% option = category['options'][j] %> + + % endfor + + % endfor +
${category['description']} +
+ ${option['text']} +
[${option['points']} points]
+
+
\ No newline at end of file From 9a752f51c74e93533f3a4e16c182443242f8d75c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 12:48:15 -0500 Subject: [PATCH 208/329] Fix rubric return, add in score field --- common/lib/xmodule/xmodule/open_ended_module.py | 2 +- lms/templates/open_ended_rubric.html | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e4f74a9124..caa4701f14 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -606,7 +606,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): def render_rubric(self, rubric_xml): rubric_categories = OpenEndedModule.extract_rubric_categories(rubric_xml) - html = render_to_string('open_ended_rubric.html', rubric_categories) + html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) return html @staticmethod diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index f89cecee80..36de4fff2e 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -1,8 +1,9 @@ - % for i in range(len(categories)): - <% category = categories[i] %> + % for i in range(len(rubric_categories)): + <% category = rubric_categories[i] %> + % for j in range(len(category['options'])): <% option = category['options'][j] %> - + % for j in range(len(category['options'])): <% option = category['options'][j] %> - + % if category['has_score'] == True: + + % endif % for j in range(len(category['options'])): <% option = category['options'][j] %> % if category['has_score'] == True: +
+ % endif % for j in range(len(category['options'])): <% option = category['options'][j] %> % endfor From 16f06ae7b79a0feecf7e3610b587fe90d67bb542 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 11:57:09 -0500 Subject: [PATCH 216/329] Fix some minor html --- lms/templates/open_ended_rubric.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index 889e771263..0048ff578d 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -4,9 +4,9 @@ % if category['has_score'] == True: -
-
- + % endif % for j in range(len(category['options'])): <% option = category['options'][j] %> From a64806656a4634fc17c17426dce0381361f97909 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 12:20:27 -0500 Subject: [PATCH 217/329] Integrate rubric into self assessment --- lms/templates/open_ended_rubric.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index 0048ff578d..9f8a2ece4e 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -2,12 +2,12 @@ % for i in range(len(rubric_categories)): <% category = rubric_categories[i] %> - - % if category['has_score'] == True: - - % endif + % for j in range(len(category['options'])): <% option = category['options'][j] %>
${category['description']}${category['score']} From 682ac3455b21387befc718212747cba1bb2fd342 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 15:51:37 -0500 Subject: [PATCH 209/329] Address some comments from code review, fix rubric display --- .../xmodule/combined_open_ended_module.py | 28 ++++++++++++++----- .../lib/xmodule/xmodule/open_ended_module.py | 8 +++--- common/lib/xmodule/xmodule/openendedchild.py | 4 +-- lms/templates/open_ended_rubric.html | 2 +- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 17355e9ce2..15bc72abb4 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -52,6 +52,11 @@ class CombinedOpenEndedModule(XModule): 'reset' -- resets the whole combined open ended module and returns to the first child module 'next_problem' -- moves to the next child module 'get_results' -- gets results from a given child module + + Types of children. Task is synonymous with child module, so each combined open ended module + incorporates multiple children (tasks): + openendedmodule + selfassessmentmodule """ STATE_VERSION = 1 @@ -60,7 +65,6 @@ class CombinedOpenEndedModule(XModule): ASSESSING = 'assessing' INTERMEDIATE_DONE = 'intermediate_done' DONE = 'done' - TASK_TYPES = ["self", "ml", "instructor", "peer"] js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'), @@ -216,23 +220,33 @@ class CombinedOpenEndedModule(XModule): current_task_type = self.get_tag_name(self.current_task_xml) children = self.child_modules() + child_task_module = children['modules'][current_task_type] self.current_task_descriptor = children['descriptors'][current_task_type](self.system) + + #This is the xml object created from the xml definition of the current task etree_xml = etree.fromstring(self.current_task_xml) + #This sends the etree_xml object through the descriptor module of the current task, and + #returns the xml parsed by the descriptor self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system) if current_task_state is None and self.current_task_number == 0: - self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task = child_task_module(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data) self.task_states.append(self.current_task.get_instance_state()) self.state = self.ASSESSING elif current_task_state is None and self.current_task_number > 0: last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] - current_task_state = ( - '{"state": "' + str(self.ASSESSING) + '", "version": 1, "max_score": ' + str(self._max_score) + ', ' + - '"attempts": 0, "created": "True", "history": [{"answer": "' + str(last_response) + '"}]}') - self.current_task = children['modules'][current_task_type](self.system, self.location, + current_task_state=json.dumps({ + 'state' : self.assessing, + 'version' : self.STATE_VERSION, + 'max_score' : self._max_score, + 'attempts' : 0, + 'created' : True, + 'history' : [{'answer' : str(last_response)}], + }) + self.current_task = child_task_module(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) self.task_states.append(self.current_task.get_instance_state()) @@ -240,7 +254,7 @@ class CombinedOpenEndedModule(XModule): else: if self.current_task_number > 0 and not reset: current_task_state = self.overwrite_state(current_task_state) - self.current_task = children['modules'][current_task_type](self.system, self.location, + self.current_task = child_task_module(self.system, self.location, self.current_task_parsed_xml, self.current_task_descriptor, self.static_data, instance_state=current_task_state) diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index caa4701f14..e6acb6409c 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -78,10 +78,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self._parse(oeparam, self.prompt, self.rubric, system) - if self.created == "True" and self.state == self.ASSESSING: - self.created = "False" + if self.created == True and self.state == self.ASSESSING: + self.created = False self.send_to_grader(self.latest_answer(), system) - self.created = "False" + self.created = False def _parse(self, oeparam, prompt, rubric, system): ''' @@ -379,7 +379,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): log.debug(response_items) rubric_feedback="" feedback = self._convert_longform_feedback_to_html(response_items) - if response_items['rubric_scores_complete']: + if response_items['rubric_scores_complete']==True: rubric_feedback = self.render_rubric(response_items['rubric_xml']) if not response_items['success']: diff --git a/common/lib/xmodule/xmodule/openendedchild.py b/common/lib/xmodule/xmodule/openendedchild.py index aa83a35c9d..2ba9528237 100644 --- a/common/lib/xmodule/xmodule/openendedchild.py +++ b/common/lib/xmodule/xmodule/openendedchild.py @@ -86,7 +86,7 @@ class OpenEndedChild(): self.state = instance_state.get('state', self.INITIAL) - self.created = instance_state.get('created', "False") + self.created = instance_state.get('created', False) self.attempts = instance_state.get('attempts', 0) self.max_attempts = static_data['max_attempts'] @@ -171,7 +171,7 @@ class OpenEndedChild(): 'state': self.state, 'max_score': self._max_score, 'attempts': self.attempts, - 'created': "False", + 'created': False, } return json.dumps(state) diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index 36de4fff2e..c6310dea4d 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -3,7 +3,7 @@ <% category = rubric_categories[i] %>
${category['description']}${category['score']}Your Score: ${category['score']} From 7d0bb7b3fe120a6a7e75cba4f40c4bc46d938aeb Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 11 Jan 2013 16:02:09 -0500 Subject: [PATCH 210/329] Minor string to boolean fix --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 15bc72abb4..cd0be0aa6e 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -173,7 +173,7 @@ class CombinedOpenEndedModule(XModule): loaded_task_state = json.loads(current_task_state) if loaded_task_state['state'] == self.INITIAL: loaded_task_state['state'] = self.ASSESSING - loaded_task_state['created'] = "True" + loaded_task_state['created'] = True loaded_task_state['history'].append({'answer': last_response}) current_task_state = json.dumps(loaded_task_state) return current_task_state From c7339ee6644e49121637ba7c888f64906f11056b Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 10:16:57 -0500 Subject: [PATCH 211/329] Minor bug fix --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index cd0be0aa6e..a88acc6ffd 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -239,7 +239,7 @@ class CombinedOpenEndedModule(XModule): last_response_data = self.get_last_response(self.current_task_number - 1) last_response = last_response_data['response'] current_task_state=json.dumps({ - 'state' : self.assessing, + 'state' : self.ASSESSING, 'version' : self.STATE_VERSION, 'max_score' : self._max_score, 'attempts' : 0, From 0567e8f0aaad9b8a61dcb9e4bfb05ec6dff46adf Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 10:35:53 -0500 Subject: [PATCH 212/329] Factor out rubric rendering --- .../xmodule/combined_open_ended_rubric.py | 116 ++++++++++++++++++ .../lib/xmodule/xmodule/open_ended_module.py | 102 +-------------- lms/templates/open_ended_rubric.html | 4 +- 3 files changed, 122 insertions(+), 100 deletions(-) create mode 100644 common/lib/xmodule/xmodule/combined_open_ended_rubric.py diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py new file mode 100644 index 0000000000..445d066416 --- /dev/null +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -0,0 +1,116 @@ +from mitxmako.shortcuts import render_to_string + +class CombinedOpenEndedRubric: + + def render_rubric(self, rubric_xml): + rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) + html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) + return html + + @staticmethod + def extract_rubric_categories(element): + ''' + Contstruct a list of categories such that the structure looks like: + [ { category: "Category 1 Name", + options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}] + }, + { category: "Category 2 Name", + options: [{text: "Option 1 Name", points: 0}, + {text: "Option 2 Name", points: 1}, + {text: "Option 3 Name", points: 2]}] + + ''' + element = etree.fromstring(element) + categories = [] + for category in element: + if category.tag != 'category': + raise Exception("[capa.inputtypes.extract_categories] Expected a tag: got {0} instead".format(category.tag)) + else: + categories.append(CombinedOpenEndedRubric.extract_category(category)) + return categories + + @staticmethod + def extract_category(category): + ''' + construct an individual category + {category: "Category 1 Name", + options: [{text: "Option 1 text", points: 1}, + {text: "Option 2 text", points: 2}]} + + all sorting and auto-point generation occurs in this function + ''' + + has_score=False + descriptionxml = category[0] + scorexml = category[1] + if score_xml.tag == "option": + optionsxml = category[1:] + else: + optionsxml = category[2:] + has_score=True + + # parse description + if descriptionxml.tag != 'description': + raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag)) + + if has_score: + if scorexml.tag != 'score': + raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag)) + + for option in optionsxml: + if option.tag != "option": + raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag)) + + description = descriptionxml.text + + if has_score: + score = int(scorexml.text) + else: + score = 0 + + cur_points = 0 + options = [] + autonumbering = True + # parse options + for option in optionsxml: + if option.tag != 'option': + raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag)) + else: + pointstr = option.get("points") + if pointstr: + autonumbering = False + # try to parse this into an int + try: + points = int(pointstr) + except ValueError: + raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr)) + elif autonumbering: + # use the generated one if we're in the right mode + points = cur_points + cur_points = cur_points + 1 + else: + raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.") + optiontext = option.text + options.append({'text': option.text, 'points': points}) + + # sort and check for duplicates + options = sorted(options, key=lambda option: option['points']) + CombinedOpenEndedRubric.validate_options(options) + + return {'description': description, 'options': options, 'score' : score, 'has_score' : has_score} + + @staticmethod + def validate_options(options): + ''' + Validates a set of options. This can and should be extended to filter out other bad edge cases + ''' + if len(options) == 0: + raise Exception("[extract_category]: no options associated with this category") + if len(options) == 1: + return + prev = options[0]['points'] + for option in options[1:]: + if prev == option['points']: + raise Exception("[extract_category]: found duplicate point values between two different options") + else: + prev = option['points'] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_module.py index e6acb6409c..11f96c9848 100644 --- a/common/lib/xmodule/xmodule/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_module.py @@ -35,6 +35,8 @@ from numpy import median from datetime import datetime +from combined_open_ended_rubric import CombinedOpenEndedRubric + log = logging.getLogger("mitx.courseware") class OpenEndedModule(openendedchild.OpenEndedChild): @@ -380,7 +382,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): rubric_feedback="" feedback = self._convert_longform_feedback_to_html(response_items) if response_items['rubric_scores_complete']==True: - rubric_feedback = self.render_rubric(response_items['rubric_xml']) + rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml']) if not response_items['success']: return system.render_template("open_ended_error.html", @@ -604,104 +606,6 @@ class OpenEndedModule(openendedchild.OpenEndedChild): html = system.render_template('open_ended.html', context) return html - def render_rubric(self, rubric_xml): - rubric_categories = OpenEndedModule.extract_rubric_categories(rubric_xml) - html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) - return html - - @staticmethod - def extract_rubric_categories(element): - ''' - Contstruct a list of categories such that the structure looks like: - [ { category: "Category 1 Name", - options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}] - }, - { category: "Category 2 Name", - options: [{text: "Option 1 Name", points: 0}, - {text: "Option 2 Name", points: 1}, - {text: "Option 3 Name", points: 2]}] - - ''' - element = etree.fromstring(element) - categories = [] - for category in element: - if category.tag != 'category': - raise Exception("[capa.inputtypes.extract_categories] Expected a tag: got {0} instead".format(category.tag)) - else: - categories.append(OpenEndedModule.extract_category(category)) - return categories - - @staticmethod - def extract_category(category): - ''' - construct an individual category - {category: "Category 1 Name", - options: [{text: "Option 1 text", points: 1}, - {text: "Option 2 text", points: 2}]} - - all sorting and auto-point generation occurs in this function - ''' - descriptionxml = category[0] - scorexml = category[1] - optionsxml = category[2:] - - # parse description - if descriptionxml.tag != 'description': - raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag)) - - if scorexml.tag != 'score': - raise Exception("[extract_category]: expected score tag, got {0} instead".format(descriptionxml.tag)) - - description = descriptionxml.text - score = int(scorexml.text) - - cur_points = 0 - options = [] - autonumbering = True - # parse options - for option in optionsxml: - if option.tag != 'option': - raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag)) - else: - pointstr = option.get("points") - if pointstr: - autonumbering = False - # try to parse this into an int - try: - points = int(pointstr) - except ValueError: - raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr)) - elif autonumbering: - # use the generated one if we're in the right mode - points = cur_points - cur_points = cur_points + 1 - else: - raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.") - optiontext = option.text - options.append({'text': option.text, 'points': points}) - - # sort and check for duplicates - options = sorted(options, key=lambda option: option['points']) - OpenEndedModule.validate_options(options) - - return {'description': description, 'options': options, 'score' : score} - - @staticmethod - def validate_options(options): - ''' - Validates a set of options. This can and should be extended to filter out other bad edge cases - ''' - if len(options) == 0: - raise Exception("[extract_category]: no options associated with this category") - if len(options) == 1: - return - prev = options[0]['points'] - for option in options[1:]: - if prev == option['points']: - raise Exception("[extract_category]: found duplicate point values between two different options") - else: - prev = option['points'] - class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): """ diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index c6310dea4d..3eee9600e2 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -3,7 +3,9 @@ <% category = rubric_categories[i] %>
${category['description']}Your Score: ${category['score']} Your Score: ${category['score']} From 2b4929562a046d4d7a1fa2954309fd4397c797a2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 10:39:58 -0500 Subject: [PATCH 213/329] Fix up rubric rendering --- .../lib/xmodule/xmodule/combined_open_ended_rubric.py | 10 +++++++--- common/lib/xmodule/xmodule/self_assessment_module.py | 6 +++++- lms/templates/self_assessment_rubric.html | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index 445d066416..2dd3eb587b 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -2,9 +2,13 @@ from mitxmako.shortcuts import render_to_string class CombinedOpenEndedRubric: - def render_rubric(self, rubric_xml): - rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) - html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) + @staticmethod + def render_rubric(rubric_xml): + try: + rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) + html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) + except: + html = rubric_xml return html @staticmethod diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 870f3ea169..940b61c557 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -21,6 +21,8 @@ from .xml_module import XmlDescriptor from xmodule.modulestore import Location import openendedchild +from combined_open_ended_rubric import CombinedOpenEndedRubric + log = logging.getLogger("mitx.courseware") class SelfAssessmentModule(openendedchild.OpenEndedChild): @@ -120,8 +122,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.state == self.INITIAL: return '' + rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric) + # we'll render it - context = {'rubric': self.rubric, + context = {'rubric': rubric_html, 'max_score': self._max_score, } diff --git a/lms/templates/self_assessment_rubric.html b/lms/templates/self_assessment_rubric.html index 5bcb3bba93..2d32ffe8d3 100644 --- a/lms/templates/self_assessment_rubric.html +++ b/lms/templates/self_assessment_rubric.html @@ -1,7 +1,7 @@

Self-assess your answer with this rubric:

- ${rubric} + ${rubric | n }
% if not read_only: From 59e5b494a9e3ad8b89bab40f6ffb4766f149b625 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 10:51:17 -0500 Subject: [PATCH 214/329] Proper rubric rendering for self assessment --- common/lib/xmodule/xmodule/combined_open_ended_rubric.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index 2dd3eb587b..a0ce900756 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -1,4 +1,8 @@ from mitxmako.shortcuts import render_to_string +import logging +from lxml import etree + +log=logging.getLogger(__name__) class CombinedOpenEndedRubric: @@ -8,6 +12,7 @@ class CombinedOpenEndedRubric: rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) except: + log.exception("Could not parse the rubric.") html = rubric_xml return html @@ -47,7 +52,7 @@ class CombinedOpenEndedRubric: has_score=False descriptionxml = category[0] scorexml = category[1] - if score_xml.tag == "option": + if scorexml.tag == "option": optionsxml = category[1:] else: optionsxml = category[2:] From 3e0cf9d234a0df720b6cc2e4a4ad4c50538d1655 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 11:53:23 -0500 Subject: [PATCH 215/329] Make rubric render more nicely --- .../xmodule/combined_open_ended_rubric.py | 6 +- .../css/combinedopenended/display.scss | 92 ++++++++++--------- lms/templates/open_ended_rubric.html | 12 ++- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index a0ce900756..0b2ca1ca2c 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -100,7 +100,11 @@ class CombinedOpenEndedRubric: else: raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.") optiontext = option.text - options.append({'text': option.text, 'points': points}) + selected = False + if has_score: + if points == score: + selected = True + options.append({'text': option.text, 'points': points, 'selected' : selected}) # sort and check for duplicates options = sorted(options, key=lambda option: option['points']) diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 75e87e1f07..a58e30f1e2 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -219,6 +219,53 @@ div.result-container { } } +div.result-container, section.open-ended-child { + .rubric { + tr { + margin:10px 0px; + height: 100%; + } + td { + padding: 20px 0px; + margin: 10px 0px; + height: 100%; + } + th { + padding: 5px; + margin: 5px; + } + label, + .view-only { + margin:10px; + position: relative; + padding: 15px; + width: 200px; + height:100%; + display: inline-block; + min-height: 50px; + min-width: 50px; + background-color: #CCC; + font-size: 1em; + } + .grade { + position: absolute; + bottom:0px; + right:0px; + margin:10px; + } + .selected-grade { + background: #666; + color: white; + } + input[type=radio]:checked + label { + background: #666; + color: white; } + input[class='score-selection'] { + display: none; + } + } +} + section.open-ended-child { @media print { display: block; @@ -576,49 +623,4 @@ section.open-ended-child { font-size: 0.9em; } - .rubric { - tr { - margin:10px 0px; - height: 100%; - } - td { - padding: 20px 0px; - margin: 10px 0px; - height: 100%; - } - th { - padding: 5px; - margin: 5px; - } - label, - .view-only { - margin:10px; - position: relative; - padding: 15px; - width: 200px; - height:100%; - display: inline-block; - min-height: 50px; - min-width: 50px; - background-color: #CCC; - font-size: 1em; - } - .grade { - position: absolute; - bottom:0px; - right:0px; - margin:10px; - } - .selected-grade { - background: #666; - color: white; - } - input[type=radio]:checked + label { - background: #666; - color: white; } - input[class='score-selection'] { - display: none; - } - } - } diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index 3eee9600e2..889e771263 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -4,14 +4,24 @@
${category['description']}Your Score: ${category['score']}
${option['text']} -
[${option['points']} points]
+ % if option.has_key('selected'): + % if option['selected'] == True: +
[${option['points']} points]
+ %else: +
[${option['points']} points]
+ % endif + % else: +
[${option['points']} points]
+ %endif
${category['description']}Your Score: ${category['score']} + Your Score: ${category['score']} +
${category['description']} - Your Score: ${category['score']} - + ${category['description']} + % if category['has_score'] == True: + (Your score: ${category['score']}) + % endif + From 0e11deffcf8279501a70ae3047bfd19fdc12c018 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 12:31:50 -0500 Subject: [PATCH 218/329] Fix staff and peer grading settings to not fail catastrophically --- lms/envs/aws.py | 4 ++-- lms/envs/common.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 0516bddc56..98c65e7cb4 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -76,8 +76,8 @@ DATABASES = AUTH_TOKENS['DATABASES'] XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] -STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE') - +STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', STAFF_GRADING_INTERFACE) +PEER_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', PEER_GRADING_INTERFACE) PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") diff --git a/lms/envs/common.py b/lms/envs/common.py index 88cf09502d..0364a8b6f8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -329,12 +329,28 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Staff grading config ##################### -STAFF_GRADING_INTERFACE = None +#By setting up the default settings with an incorrect user name and password, +# will get an error when attempting to connect +STAFF_GRADING_INTERFACE = { + 'url': 'http://sandbox-grader-001.m.edx.org/staff_grading', + 'username': 'incorrect_user', + 'password': 'incorrect_pass', + } + # Used for testing, debugging MOCK_STAFF_GRADING = False ################################# Peer grading config ##################### -PEER_GRADING_INTERFACE = None + +#By setting up the default settings with an incorrect user name and password, +# will get an error when attempting to connect +PEER_GRADING_INTERFACE = { + 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading', + 'username': 'incorrect_user', + 'password': 'incorrect_pass', + } + +# Used for testing, debugging MOCK_PEER_GRADING = False ################################# Jasmine ################################### From 1316728aaa851ad6666acfa77c90eaf5092eecd5 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 12:32:31 -0500 Subject: [PATCH 219/329] Minor naming fix --- lms/envs/aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 98c65e7cb4..7b8c48f4af 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -77,7 +77,7 @@ DATABASES = AUTH_TOKENS['DATABASES'] XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', STAFF_GRADING_INTERFACE) -PEER_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', PEER_GRADING_INTERFACE) +PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_INTERFACE) PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") From 19f79fa36406038031abf62c1bd2b138aa8ce9d1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 14 Jan 2013 12:40:17 -0500 Subject: [PATCH 220/329] Prune deleted remote branches --- jenkins/quality.sh | 2 ++ jenkins/test.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jenkins/quality.sh b/jenkins/quality.sh index 4cf26d76bf..56217af874 100755 --- a/jenkins/quality.sh +++ b/jenkins/quality.sh @@ -3,6 +3,8 @@ set -e set -x +git remote prune origin + # Reset the submodule, in case it changed git submodule foreach 'git reset --hard HEAD' diff --git a/jenkins/test.sh b/jenkins/test.sh index 8a96024785..7a61e914b7 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -15,6 +15,8 @@ function github_mark_failed_on_exit { trap '[ $? == "0" ] || github_status state:failure "failed"' EXIT } +git remote prune origin + github_mark_failed_on_exit github_status state:pending "is running" From ad1e73fa861e3cae07ad2cf4c350f400896ed6ab Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 14 Jan 2013 13:12:38 -0500 Subject: [PATCH 221/329] Clean up capa by removing message post actions that aren't needed --- common/lib/capa/capa/capa_problem.py | 18 ------------------ common/lib/xmodule/xmodule/capa_module.py | 15 --------------- 2 files changed, 33 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index efc96fc717..2eaa0e4286 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -186,24 +186,6 @@ class LoncapaProblem(object): maxscore += responder.get_max_score() return maxscore - def message_post(self,event_info): - """ - Handle an ajax post that contains feedback on feedback - Returns a boolean success variable - Note: This only allows for feedback to be posted back to the grading controller for the first - open ended response problem on each page. Multiple problems will cause some sync issues. - TODO: Handle multiple problems on one page sync issues. - """ - success=False - message = "Could not find a valid responder." - log.debug("in lcp") - for responder in self.responders.values(): - if hasattr(responder, 'handle_message_post'): - success, message = responder.handle_message_post(event_info) - if success: - break - return success, message - def get_score(self): """ Compute score for this problem. The score is the number of points awarded. diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 08f503f127..1da271072a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -380,7 +380,6 @@ class CapaModule(XModule): 'problem_save': self.save_problem, 'problem_show': self.get_answer, 'score_update': self.update_score, - 'message_post' : self.message_post, } if dispatch not in handlers: @@ -395,20 +394,6 @@ class CapaModule(XModule): }) return json.dumps(d, cls=ComplexEncoder) - def message_post(self, get): - """ - Posts a message from a form to an appropriate location - """ - event_info = dict() - event_info['state'] = self.lcp.get_state() - event_info['problem_id'] = self.location.url() - event_info['student_id'] = self.system.anonymous_student_id - event_info['survey_responses']= get - - success, message = self.lcp.message_post(event_info) - - return {'success' : success, 'message' : message} - def closed(self): ''' Is the student still allowed to submit answers? ''' if self.attempts == self.max_attempts: From 833b0c34abc4a93c25abcb0cd29e4734c2e42fc1 Mon Sep 17 00:00:00 2001 From: Jennifer Akana Date: Mon, 14 Jan 2013 19:20:49 -0500 Subject: [PATCH 222/329] added problem weight description --- doc/course_grading.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/course_grading.md b/doc/course_grading.md index 6dce2fa70e..5c668df5d9 100644 --- a/doc/course_grading.md +++ b/doc/course_grading.md @@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively. Note that the default weight of a problem **is not 1.** The default weight of a problem is the module's max_grade. +If weighting is set, each problem is worth the number of points assigned, regardless of the number of responses it contains. + +Consider a Homework section that contains two problems. + + + ... + + +and + + + ... + ... + ... + + + + + + +Without weighting, Problem 1 is worth 25% of the assignment, and Problem 2 is worth 75% of the assignment. + +Weighting for the problems can be set in the policy.json file. + + "problem/problem1": { + "weight": 2 + }, + "problem/problem2": { + "weight": 2 + }, + +With the above weighting, Problems 1 and 2 are each worth 50% of the assignment. + +Please note: When problems have weight, the point value is automatically included in the display name *except* when “weight”: 1.When “weight”: 1, no visual change occurs in the display name, leaving the point value open to interpretation to the student. + + + ## Section Weighting Once each section has a percentage score, we must total those sections into a From c5107d42de7d3cafef4c7f0fec8f7785aa7f50fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 17:38:59 +0200 Subject: [PATCH 223/329] bug --- common/lib/xmodule/setup.py | 1 + common/lib/xmodule/xmodule/gst_module.py | 141 +++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 common/lib/xmodule/xmodule/gst_module.py diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index c867fca228..8deb2a7ce5 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,6 +36,7 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", + "gst = xmodule.gst_module:GSTDescriptor", ] } ) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py new file mode 100644 index 0000000000..8847a5224c --- /dev/null +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -0,0 +1,141 @@ +""" +GST (Graphical-Slider-Tool) module is ungraded xmodule used by students to +understand functional dependencies +""" + +# import json +import logging + +from lxml import etree + +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.x_module import XModule +from xmodule.progress import Progress +from xmodule.exceptions import NotFoundError +from pkg_resources import resource_string +from xmodule.raw_module import RawDescriptor + +# log = logging.getLogger("mitx.common.lib.seq_module") + + +class GSTModule(XModule): + ''' Graphical-Slider-Tool Module + ''' + # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} + # #css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} + # js_module_name = "GST" + + def __init__(self, system, location, definition, descriptor, instance_state=None, + shared_state=None, **kwargs): + """ + pass + # Definition should have.... + # sliders, text, module + + # Sample file: + + # + #

Plot...

+ # + # + # + # + # + # + # + # + #
+ # """ + # XModule.__init__(self, system, location, definition, descriptor, + # instance_state, shared_state, **kwargs) + # import ipdb; ipdb.set_trace() + # self.rendered = False + + # def get_html(self): + # self.render() + # return self.content + + # def render(self): + # import ipdb; ipdb.set_trace() + # if self.rendered: + # return + # ## Returns a set of all types of all sub-children + # contents = [] + # # import ipdb; ipdb.set_trace() + # for child in self.get_display_items(): + # progress = child.get_progress() + # childinfo = { + # 'gst': child.get_html(), + # 'plot': "\n".join( + # grand_child.display_name.strip() + # for grand_child in child.get_children() + # if 'display_name' in grand_child.metadata + # ), + # # 'progress_status': Progress.to_js_status_str(progress), + # 'progress_detail': Progress.to_js_detail_str(progress), + # 'type': child.get_icon_class(), + # } + # # if childinfo['title']=='': + # # childinfo['title'] = child.metadata.get('display_name','') + # contents.append(childinfo) + + # params = {'items': contents, + # 'element_id': self.location.html_id(), + # 'item_id': self.id, + # 'position': self.position, + # 'tag': self.location.category + # } + + # self.content = self.system.render_template('seq_module.html', params) + # self.rendered = True + + +class GSTDescriptor(RawDescriptor): + mako_template = "widgets/html-edit.html" + module_class = GSTModule + template_dir_name = 'gst' + + # @classmethod + # def definition_from_xml(cls, xml_object, system): + # """ + # Pull out the data into dictionary. + + # Returns: + # { + # 'def1': 'def1-some-html', + # 'def2': 'def2-some-html' + # } + # """ + # import ipdb; ipdb.set_trace() + # children = [] + # for child in xml_object: + # try: + # children.append(system.process_xml(etree.tostring(child)).location.url()) + # except: + # log.exception("Unable to load child when parsing GST. Continuing...") + # continue + # return {'children': children} + + # def definition_to_xml(self, resource_fs): + # '''Return an xml element representing this definition.''' + # import ipdb; ipdb.set_trace() + # xml_object = etree.Element('gst') + + # def add_child(k): + # # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + # child_str = child.export_to_xml(resource_fs) + # child_node = etree.fromstring(child_str) + # xml_object.append(child_node) + + # for child in self.get_children(): + # add_child(child) + + # return xml_object + + + # def __init__(self, system, definition, **kwargs): + # '''Render and save the template for this descriptor instance''' + # super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) + # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From d0fcaf0ac4f2de72ce696e125905184eab934ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 18:11:01 +0200 Subject: [PATCH 224/329] next step --- common/lib/xmodule/xmodule/gst_module.py | 71 ++++++++++++------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 8847a5224c..285eeb8fce 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -97,45 +97,46 @@ class GSTDescriptor(RawDescriptor): module_class = GSTModule template_dir_name = 'gst' - # @classmethod - # def definition_from_xml(cls, xml_object, system): - # """ - # Pull out the data into dictionary. + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Pull out the data into dictionary. - # Returns: - # { - # 'def1': 'def1-some-html', - # 'def2': 'def2-some-html' - # } - # """ - # import ipdb; ipdb.set_trace() - # children = [] - # for child in xml_object: - # try: - # children.append(system.process_xml(etree.tostring(child)).location.url()) - # except: - # log.exception("Unable to load child when parsing GST. Continuing...") - # continue - # return {'children': children} + Returns: + { + 'def1': 'def1-some-html', + 'def2': 'def2-some-html' + } + """ + import ipdb; ipdb.set_trace() + children = [] + for child in xml_object: + try: + children.append(system.process_xml(etree.tostring(child)).location.url()) + except: + log.exception("Unable to load child when parsing GST. Continuing...") + continue + return {'children': children} - # def definition_to_xml(self, resource_fs): - # '''Return an xml element representing this definition.''' - # import ipdb; ipdb.set_trace() - # xml_object = etree.Element('gst') + def definition_to_xml(self, resource_fs): + '''Return an xml element representing this definition.''' + import ipdb; ipdb.set_trace() + xml_object = etree.Element('gst') - # def add_child(k): - # # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) - # child_str = child.export_to_xml(resource_fs) - # child_node = etree.fromstring(child_str) - # xml_object.append(child_node) + def add_child(k): + # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + child_str = child.export_to_xml(resource_fs) + child_node = etree.fromstring(child_str) + xml_object.append(child_node) - # for child in self.get_children(): - # add_child(child) + for child in self.get_children(): + add_child(child) - # return xml_object + return xml_object - # def __init__(self, system, definition, **kwargs): - # '''Render and save the template for this descriptor instance''' - # super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) - # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file + def __init__(self, system, definition, **kwargs): + '''Render and save the template for this descriptor instance''' + import ipdb; ipdb.set_trace() + super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) + self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 884db888f953d651d18a1f4cf5445da436451bdd Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 28 Nov 2012 16:48:07 +0200 Subject: [PATCH 225/329] teting gst --- common/lib/xmodule/xmodule/gst_module.py | 128 +++++++++++------------ 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 285eeb8fce..f5b2095d95 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -29,70 +29,70 @@ class GSTModule(XModule): def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): """ - pass - # Definition should have.... - # sliders, text, module + Definition should have.... + sliders, text, module - # Sample file: + Sample file: - # - #

Plot...

- # - # - # - # - # - # - # - # - #
- # """ - # XModule.__init__(self, system, location, definition, descriptor, - # instance_state, shared_state, **kwargs) - # import ipdb; ipdb.set_trace() - # self.rendered = False + +

Plot...

+ + + + + + + + +
+ """ + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + # import ipdb; ipdb.set_trace() + # self.rendered = False - # def get_html(self): - # self.render() - # return self.content + def get_html(self): + self.render() + return self.content - # def render(self): - # import ipdb; ipdb.set_trace() - # if self.rendered: - # return - # ## Returns a set of all types of all sub-children - # contents = [] - # # import ipdb; ipdb.set_trace() - # for child in self.get_display_items(): - # progress = child.get_progress() - # childinfo = { - # 'gst': child.get_html(), - # 'plot': "\n".join( - # grand_child.display_name.strip() - # for grand_child in child.get_children() - # if 'display_name' in grand_child.metadata - # ), - # # 'progress_status': Progress.to_js_status_str(progress), - # 'progress_detail': Progress.to_js_detail_str(progress), - # 'type': child.get_icon_class(), - # } - # # if childinfo['title']=='': - # # childinfo['title'] = child.metadata.get('display_name','') - # contents.append(childinfo) + def render(self): + # import ipdb; ipdb.set_trace() + # if self.rendered: + return + ## Returns a set of all types of all sub-children + # contents = [] + # # import ipdb; ipdb.set_trace() + # for child in self.get_display_items(): + # progress = child.get_progress() + # childinfo = { + # 'gst': child.get_html(), + # 'plot': "\n".join( + # grand_child.display_name.strip() + # for grand_child in child.get_children() + # if 'display_name' in grand_child.metadata + # ), + # # 'progress_status': Progress.to_js_status_str(progress), + # 'progress_detail': Progress.to_js_detail_str(progress), + # 'type': child.get_icon_class(), + # } + # # if childinfo['title']=='': + # # childinfo['title'] = child.metadata.get('display_name','') + # contents.append(childinfo) - # params = {'items': contents, - # 'element_id': self.location.html_id(), - # 'item_id': self.id, - # 'position': self.position, - # 'tag': self.location.category - # } - - # self.content = self.system.render_template('seq_module.html', params) - # self.rendered = True + # params = {'items': contents, + # 'element_id': self.location.html_id(), + # 'item_id': self.id, + # 'position': self.position, + # 'tag': self.location.category + # } + params= {} + self.content = self.system.render_template('gst_module.html', params) + # self.rendered = True -class GSTDescriptor(RawDescriptor): +# class GSTDescriptor(RawDescriptor): +class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): mako_template = "widgets/html-edit.html" module_class = GSTModule template_dir_name = 'gst' @@ -108,7 +108,7 @@ class GSTDescriptor(RawDescriptor): 'def2': 'def2-some-html' } """ - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() children = [] for child in xml_object: try: @@ -120,7 +120,7 @@ class GSTDescriptor(RawDescriptor): def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() xml_object = etree.Element('gst') def add_child(k): @@ -135,8 +135,8 @@ class GSTDescriptor(RawDescriptor): return xml_object - def __init__(self, system, definition, **kwargs): - '''Render and save the template for this descriptor instance''' - import ipdb; ipdb.set_trace() - super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) - self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file + # def __init__(self, system, definition, **kwargs): + # '''Render and save the template for this descriptor instance''' + # # import ipdb; ipdb.set_trace() + # super(GSTDescriptor, self).__init__(system, definition, **kwargs) + # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 07cb76a7d99e822b4dbc9c140c6ad929c2bbbbc9 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Thu, 29 Nov 2012 16:43:33 +0200 Subject: [PATCH 226/329] first working version of gst module --- common/lib/xmodule/setup.py | 2 +- common/lib/xmodule/xmodule/gst_module.py | 150 +++++++++++------------ 2 files changed, 73 insertions(+), 79 deletions(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 8deb2a7ce5..86636ef05a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,7 +36,7 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", - "gst = xmodule.gst_module:GSTDescriptor", + "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", ] } ) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index f5b2095d95..d90bc46e6e 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -1,6 +1,6 @@ """ -GST (Graphical-Slider-Tool) module is ungraded xmodule used by students to -understand functional dependencies +Graphical slider tool module is ungraded xmodule used by students to +understand functional dependencies. """ # import json @@ -15,11 +15,12 @@ from xmodule.progress import Progress from xmodule.exceptions import NotFoundError from pkg_resources import resource_string from xmodule.raw_module import RawDescriptor +from xmodule.stringify import stringify_children # log = logging.getLogger("mitx.common.lib.seq_module") -class GSTModule(XModule): +class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} @@ -34,92 +35,92 @@ class GSTModule(XModule): Sample file: - -

Plot...

- - - - - - - - -
+ + + + + Graphic slider tool html. Can include + 'number', 'slider' and plot tags. They will be replaced + by proper number, slider and plot widgets. + + + + + + + + + + + + + -10, 1, 10 + + 1 + 1 + + + + + + """ XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) - # import ipdb; ipdb.set_trace() - # self.rendered = False def get_html(self): - self.render() + params = { + 'main_html': self.definition['render'].strip(), + 'element_id': self.location.html_id(), + 'element_class': self.location.category + } + self.content = (self.system.render_template( + 'graphical_slider_tool.html', params)) return self.content - def render(self): - # import ipdb; ipdb.set_trace() - # if self.rendered: - return - ## Returns a set of all types of all sub-children - # contents = [] - # # import ipdb; ipdb.set_trace() - # for child in self.get_display_items(): - # progress = child.get_progress() - # childinfo = { - # 'gst': child.get_html(), - # 'plot': "\n".join( - # grand_child.display_name.strip() - # for grand_child in child.get_children() - # if 'display_name' in grand_child.metadata - # ), - # # 'progress_status': Progress.to_js_status_str(progress), - # 'progress_detail': Progress.to_js_detail_str(progress), - # 'type': child.get_icon_class(), - # } - # # if childinfo['title']=='': - # # childinfo['title'] = child.metadata.get('display_name','') - # contents.append(childinfo) - # params = {'items': contents, - # 'element_id': self.location.html_id(), - # 'item_id': self.id, - # 'position': self.position, - # 'tag': self.location.category - # } - params= {} - self.content = self.system.render_template('gst_module.html', params) - # self.rendered = True - - -# class GSTDescriptor(RawDescriptor): -class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): - mako_template = "widgets/html-edit.html" - module_class = GSTModule - template_dir_name = 'gst' +class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): + module_class = GraphicalSliderToolModule + template_dir_name = 'graphical_slider_tool' @classmethod def definition_from_xml(cls, xml_object, system): """ Pull out the data into dictionary. + Args: + xml_object: xml from file. + Returns: - { - 'def1': 'def1-some-html', - 'def2': 'def2-some-html' - } + dict """ - # import ipdb; ipdb.set_trace() - children = [] - for child in xml_object: - try: - children.append(system.process_xml(etree.tostring(child)).location.url()) - except: - log.exception("Unable to load child when parsing GST. Continuing...") - continue - return {'children': children} + # check for presense of required tags in xml + expected_children_level_0 = ['render', 'configuration'] + for child in expected_children_level_0: + if len(xml_object.xpath(child)) != 1: + raise ValueError("Self a\ssessment definition must include \ + exactly one '{0}' tag".format(child)) + expected_children_level_1 = ['plot'] + for child in expected_children_level_1: + if len(xml_object.xpath('configuration')[0].xpath(child)) != 1: + raise ValueError("Self a\ssessment definition must include \ + exactly one '{0}' tag".format(child)) + # finished + + def parse(k): + """Assumes that xml_object has child k""" + return stringify_children(xml_object.xpath(k)[0]) + + return { + 'render': parse('render'), + 'configuration': xml_object.xpath('configuration')[0], + } def definition_to_xml(self, resource_fs): - '''Return an xml element representing this definition.''' + '''Return an xml element representing this definition. + Not implemented''' # import ipdb; ipdb.set_trace() xml_object = etree.Element('gst') @@ -133,10 +134,3 @@ class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): add_child(child) return xml_object - - - # def __init__(self, system, definition, **kwargs): - # '''Render and save the template for this descriptor instance''' - # # import ipdb; ipdb.set_trace() - # super(GSTDescriptor, self).__init__(system, definition, **kwargs) - # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 54c222a0153073126a74cec3acb050a3e578e24e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Thu, 29 Nov 2012 16:44:10 +0200 Subject: [PATCH 227/329] template for gst --- lms/templates/graphical_slider_tool.html | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lms/templates/graphical_slider_tool.html diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html new file mode 100644 index 0000000000..cb484d9e6c --- /dev/null +++ b/lms/templates/graphical_slider_tool.html @@ -0,0 +1,3 @@ +
+${main_html} +
From 169d4b123ca9f803ab7b5381a4c4450ece1719a8 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 14:55:00 +0200 Subject: [PATCH 228/329] updates --- common/lib/xmodule/xmodule/gst_module.py | 50 +++++++++++++++++++----- lms/templates/graphical_slider_tool.html | 14 ++++++- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index d90bc46e6e..e98b7b6c90 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -3,21 +3,19 @@ Graphical slider tool module is ungraded xmodule used by students to understand functional dependencies. """ -# import json +import json import logging - from lxml import etree +import xmltodict +import re from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.x_module import XModule -from xmodule.progress import Progress -from xmodule.exceptions import NotFoundError -from pkg_resources import resource_string -from xmodule.raw_module import RawDescriptor from xmodule.stringify import stringify_children -# log = logging.getLogger("mitx.common.lib.seq_module") + +log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): @@ -71,15 +69,49 @@ class GraphicalSliderToolModule(XModule): instance_state, shared_state, **kwargs) def get_html(self): + self.get_configuration() + gst_html = self.substitute_controls(self.definition['render'].strip()) + params = { - 'main_html': self.definition['render'].strip(), + 'gst_html': gst_html, 'element_id': self.location.html_id(), - 'element_class': self.location.category + 'element_class': self.location.category, + 'configuration_json': self.configuration_json } self.content = (self.system.render_template( 'graphical_slider_tool.html', params)) + # import ipdb; ipdb.set_trace() return self.content + def substitute_controls(self, html_string): + """ Substitue control element via their divs. + Simple variant: slider and plot controls are not inside any tag. + """ + plot_div = '
\ + This is plot
' + html_string.replace('$plot$', plot_div) + vars = [x['@var'] for x in json.loads(self.configuration_json)['root']['sliders']['slider']] + for var in vars: + m = re.match('$slider\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) + if m: + # Note: we subtract 15 to compensate for the size of the dot on the screen. + # (is a 30x30 image--lms/static/green-pointer.png). + (self.gx, self.gy) = [int(x) - 15 for x in m.groups()] + html.replace('$slider' + ' ' + x['@var']) + return html_string + + def get_configuration(self): + """Parse self.definition['configuration'] and transfer it to javascript + via json. + """ + # root added for interface compatibility with xmltodict.parse + self.configuration_json = json.dumps( + xmltodict.parse('' + + stringify_children(self.definition['configuration']) + + '')) + return self.configuration_json + class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): module_class = GraphicalSliderToolModule diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index cb484d9e6c..7d6cc292c5 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,3 +1,13 @@ -
-${main_html} +
+ +
+ + +${gst_html} + + +{# widgests
#} + +
From 69d0156c36c9316b365aa6128d09c737f3b78177 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 17:17:32 +0200 Subject: [PATCH 229/329] slider, plot and input contrlos are transferred from xml to html substituted to proper div elements --- common/lib/xmodule/xmodule/gst_module.py | 50 ++++++++++++++++++------ lms/templates/graphical_slider_tool.html | 7 +--- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index e98b7b6c90..2d65b768ec 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -70,12 +70,14 @@ class GraphicalSliderToolModule(XModule): def get_html(self): self.get_configuration() + self.html_id = self.location.html_id() + self.html_class = self.location.category gst_html = self.substitute_controls(self.definition['render'].strip()) - + params = { 'gst_html': gst_html, - 'element_id': self.location.html_id(), - 'element_class': self.location.category, + 'element_id': self.html_id, + 'element_class': self.html_class, 'configuration_json': self.configuration_json } self.content = (self.system.render_template( @@ -87,18 +89,44 @@ class GraphicalSliderToolModule(XModule): """ Substitue control element via their divs. Simple variant: slider and plot controls are not inside any tag. """ + #substitute plot plot_div = '
\ This is plot
' - html_string.replace('$plot$', plot_div) - vars = [x['@var'] for x in json.loads(self.configuration_json)['root']['sliders']['slider']] + html_string = html_string.replace('$plot$', plot_div) + + # substitute sliders + sliders = json.loads(self.configuration_json)['root']['sliders']['slider'] + if type(sliders) == dict: + sliders = [sliders] + vars = [x['@var'] for x in sliders] + + slider_div = '
This is slider
' + for var in vars: - m = re.match('$slider\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) - if m: - # Note: we subtract 15 to compensate for the size of the dot on the screen. - # (is a 30x30 image--lms/static/green-pointer.png). - (self.gx, self.gy) = [int(x) - 15 for x in m.groups()] - html.replace('$slider' + ' ' + x['@var']) + html_string = re.sub(r'\$slider\s+' + var + r'\$', + slider_div.format(element_class=self.html_class, + element_id=self.html_id, + var=var), + html_string, flags=re.IGNORECASE | re.UNICODE) + + # substitute numbers + inputs = json.loads(self.configuration_json)['root']['inputs']['input'] + if type(inputs) == dict: + inputs = [inputs] + vars = [x['@var'] for x in inputs] + + input_div = '
This is input
' + + for var in vars: + html_string = re.sub(r'\$input\s+' + var + r'\$', + input_div.format(element_class=self.html_class, + element_id=self.html_id, + var=var), + html_string, flags=re.IGNORECASE | re.UNICODE) + # import ipdb; ipdb.set_trace() return html_string def get_configuration(self): diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index 7d6cc292c5..920e53cab3 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,13 +1,10 @@
+
+data-json="${configuration_json}">
${gst_html} - -{# widgests
#} - -
From 30df77b9b24131c9e5960a81838379844c11727e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 17:21:30 +0200 Subject: [PATCH 230/329] added xmltodict to dependencies --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a3e1e3e6e5..08cfe57e2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,3 +56,4 @@ dogstatsd-python==0.2.1 sphinx==1.1.3 Shapely==1.2.16 ipython==0.13.1 +xmltodict==0.4.1 From da4a676513e201290ce02bdd36b90725163068f6 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Mon, 3 Dec 2012 15:08:37 +0200 Subject: [PATCH 231/329] working xmodule - GsT --- common/lib/xmodule/xmodule/gst_module.py | 68 +++++++++++++------ .../js/src/graphical_slider_tool/gst.js | 17 +++++ lms/templates/graphical_slider_tool.html | 12 ++++ 3 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 2d65b768ec..60c03dec10 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -13,6 +13,7 @@ from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.x_module import XModule from xmodule.stringify import stringify_children +from pkg_resources import resource_string log = logging.getLogger("mitx.common.lib.gst_module") @@ -21,9 +22,8 @@ log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' - # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} - # #css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} - # js_module_name = "GST" + js = {'js': [resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')]} + js_module_name = "GraphicalSliderTool" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -37,30 +37,55 @@ class GraphicalSliderToolModule(XModule): - Graphic slider tool html. Can include - 'number', 'slider' and plot tags. They will be replaced - by proper number, slider and plot widgets. +

Graphic slider tool html.

+

Can include 'input', 'slider' and 'plot' tags. + They will be replaced by proper number, slider and plot + widgets.

+ For example: $slider a$, second $slider b$, + number $input a$, and, plot: + $plot$ + +
- + + + - - - + + + + - - - - -10, 1, 10 - - 1 - 1 + + + + + + + + -10, 10 + + 60 + + -9, 1, 9 + -9, 1, 9 + + + + +
@@ -73,12 +98,13 @@ class GraphicalSliderToolModule(XModule): self.html_id = self.location.html_id() self.html_class = self.location.category gst_html = self.substitute_controls(self.definition['render'].strip()) - + # import ipdb; ipdb.set_trace() params = { 'gst_html': gst_html, 'element_id': self.html_id, 'element_class': self.html_class, - 'configuration_json': self.configuration_json + 'configuration_json': self.configuration_json, + 'plot_code': self.definition['plot_code'] } self.content = (self.system.render_template( 'graphical_slider_tool.html', params)) @@ -157,11 +183,12 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): dict """ # check for presense of required tags in xml - expected_children_level_0 = ['render', 'configuration'] + expected_children_level_0 = ['render', 'configuration', 'plot_code'] for child in expected_children_level_0: if len(xml_object.xpath(child)) != 1: raise ValueError("Self a\ssessment definition must include \ exactly one '{0}' tag".format(child)) + expected_children_level_1 = ['plot'] for child in expected_children_level_1: if len(xml_object.xpath('configuration')[0].xpath(child)) != 1: @@ -176,6 +203,7 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): return { 'render': parse('render'), 'configuration': xml_object.xpath('configuration')[0], + 'plot_code': parse('plot_code'), } def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js new file mode 100644 index 0000000000..03778ea437 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -0,0 +1,17 @@ +// Graphical Slider Tool module + +(function() { + this.GraphicalSliderTool = (function() { + function GST(el) { + console.log(el); + // element is : + //
+ } + // console.log('in GST'); + return GST; + + })(); +}).call(this); +// this=window, after call +// window['Graphical_Slider_Tool'] is available. \ No newline at end of file diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index 920e53cab3..fc5052893a 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -4,7 +4,19 @@
+ +
+ ${gst_html}
+ + + From 643ac69a57af84733318fd06675a3f1126661ff2 Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 16:56:31 +0200 Subject: [PATCH 232/329] Separating JS from the html for GST. --- common/static/js/graphical_slider_tool/main.js | 1 + lms/templates/graphical_slider_tool.html | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js new file mode 100644 index 0000000000..bb624d5888 --- /dev/null +++ b/common/static/js/graphical_slider_tool/main.js @@ -0,0 +1 @@ +alert('Hello, world!'); diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index fc5052893a..97fca83ff4 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -14,9 +14,9 @@ ${gst_html}
- From 0913e9ef69c6f12f5d87195c54d683de040bc82c Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 17:16:57 +0200 Subject: [PATCH 233/329] Work on GST. --- .../js/src/graphical_slider_tool/module.js | 15 ++++ .../js/graphical_slider_tool/gst_module.js | 15 ++++ .../static/js/graphical_slider_tool/main.js | 76 ++++++++++++++++++- lms/envs/common.py | 1 + lms/templates/graphical_slider_tool.html | 26 ++----- 5 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js create mode 100644 common/static/js/graphical_slider_tool/gst_module.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/static/js/graphical_slider_tool/gst_module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js index bb624d5888..da36d9c9d6 100644 --- a/common/static/js/graphical_slider_tool/main.js +++ b/common/static/js/graphical_slider_tool/main.js @@ -1 +1,75 @@ -alert('Hello, world!'); +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +// For documentation please check: +// http://requirejs.org/docs/api.html +requirejs.config({ + // Because require.js is included as a simple From 5990fa2ef5e382465f2d042efbe5a44eba5a4762 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Fri, 7 Dec 2012 17:25:24 +0200 Subject: [PATCH 234/329] Integrated RequireJS with xmodule for GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 ++++++- .../js/src/graphical_slider_tool/gst.js | 37 ++++++++++--------- .../js/src/graphical_slider_tool/gst_main.js | 17 +++++++++ .../{module.js => mod1.js} | 3 +- .../js/src/graphical_slider_tool/mod2.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod3.js | 19 ++++++++++ .../js/src/graphical_slider_tool/mod4.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod5.js | 16 ++++++++ 8 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{module.js => mod1.js} (88%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 60c03dec10..f89cb0f990 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -22,7 +22,19 @@ log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' - js = {'js': [resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')]} + + js = { + 'js': [ + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] + } js_module_name = "GraphicalSliderTool" def __init__(self, system, location, definition, descriptor, instance_state=None, diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js index 03778ea437..1434d05f70 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -1,17 +1,20 @@ -// Graphical Slider Tool module - -(function() { - this.GraphicalSliderTool = (function() { - function GST(el) { - console.log(el); - // element is : - //
- } - // console.log('in GST'); - return GST; - - })(); -}).call(this); -// this=window, after call -// window['Graphical_Slider_Tool'] is available. \ No newline at end of file +/* + * We will add a function that will be called for all GraphicalSliderTool + * xmodule module instances. It must be available globally by design of + * xmodule. + */ +window.GraphicalSliderTool = function (el) { + // All the work will be performed by the GstMain module. We will get access + // to it, and all it's dependencies, via Require JS. Currently Require JS + // is namespaced and is available via a global object RequireJS. + RequireJS.require(['GstMain'], function (GstMain) { + // The GstMain module expects the DOM ID of a Graphical Slider Tool + // element. Since we are given a
element which might in + // theory contain multiple graphical_slider_tool
elements (each + // with a unique DOM ID), we will iterate over all children, and for + // each match, we will call GstMain module. + $(el).children('.graphical_slider_tool').each(function (index, value) { + GstMain($(value).attr('id')); + }); + }); +}; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js new file mode 100644 index 0000000000..66f98eddf7 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -0,0 +1,17 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { + return GstMain; + + function GstMain(gstId) { + console.log('The DOM ID of the current GST element is ' + gstId); + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js similarity index 88% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js index c4661b5e44..44674b96d3 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js @@ -2,7 +2,8 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define([], function () { +define('mod1', [], function () { + console.log('we are in the mod1 callback'); return { 'module_status': 'OK' }; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js new file mode 100644 index 0000000000..9c26bb1dfe --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod2', [], function () { + console.log('we are in the mod2 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js new file mode 100644 index 0000000000..21961f3611 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js @@ -0,0 +1,19 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod3', ['mod5'], function (mod5) { + console.log('we are in the mod3 callback'); + + console.log('mod5 status: [' + mod5.module_status + '].'); + + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js new file mode 100644 index 0000000000..0edf809155 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod4', [], function () { + console.log('we are in the mod4 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js new file mode 100644 index 0000000000..5e843ac468 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod5', [], function () { + console.log('we are in the mod5 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) From 080e96fdc481b95f9a50348c368872baacc17a12 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 08:28:34 +0200 Subject: [PATCH 235/329] Work in progress on GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 +- .../{mod3.js => general_methods.js} | 12 +- .../js/src/graphical_slider_tool/gst_main.js | 12 +- .../js/src/graphical_slider_tool/logme.js | 54 ++++++ .../js/src/graphical_slider_tool/mod1.js | 16 -- .../js/src/graphical_slider_tool/mod2.js | 16 -- .../js/src/graphical_slider_tool/mod4.js | 16 -- .../js/src/graphical_slider_tool/sliders.js | 142 +++++++++++++++ .../js/src/graphical_slider_tool/state.js | 165 ++++++++++++++++++ lms/templates/graphical_slider_tool.html | 16 +- 10 files changed, 395 insertions(+), 68 deletions(-) rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{mod3.js => general_methods.js} (65%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index f89cb0f990..3d7b8a9f02 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -26,10 +26,10 @@ class GraphicalSliderToolModule(XModule): js = { 'js': [ resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -128,7 +128,7 @@ class GraphicalSliderToolModule(XModule): Simple variant: slider and plot controls are not inside any tag. """ #substitute plot - plot_div = '
\ This is plot
' html_string = html_string.replace('$plot$', plot_div) @@ -139,7 +139,7 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
This is input
' for var in vars: diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js similarity index 65% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js index 21961f3611..9cdd4fff0f 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js @@ -2,12 +2,16 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('mod3', ['mod5'], function (mod5) { - console.log('we are in the mod3 callback'); - - console.log('mod5 status: [' + mod5.module_status + '].'); +define('GeneralMethods', [], function () { + if (!String.prototype.trim) { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + String.prototype.trim = function trim(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }; + } return { + 'module_name': 'GeneralMethods', 'module_status': 'OK' }; }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 66f98eddf7..9f2c4c356d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,11 +2,19 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { +define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { + logme(GeneralMethods); + return GstMain; function GstMain(gstId) { - console.log('The DOM ID of the current GST element is ' + gstId); + var config, state; + + config = JSON.parse($('#' + gstId + '_json').html()).root; + + state = State(gstId, config); + + Sliders(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js new file mode 100644 index 0000000000..c045757044 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js @@ -0,0 +1,54 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('logme', [], function () { + var debugMode; + + // debugMode can be one of the following: + // + // true - All messages passed to logme will be written to the internal + // browser console. + // false - Suppress all output to the internal browser console. + // + // Obviously, if anywhere there is a direct console.log() call, we can't do + // anything about it. That's why use logme() - it will allow to turn off + // the output of debug information with a single change to a variable. + debugMode = true; + + return logme; + + /* + * function: logme + * + * A helper function that provides logging facilities. We don't want + * to call console.log() directly, because sometimes it is not supported + * by the browser. Also when everything is routed through this function. + * the logging output can be easily turned off. + * + * logme() supports multiple parameters. Each parameter will be passed to + * console.log() function separately. + * + */ + function logme() { + var i; + + if ( + (typeof debugMode === 'undefined') || + (debugMode !== true) || + (typeof window.console === 'undefined') + ) { + return; + } + + for (i = 0; i < arguments.length; i++) { + window.console.log(arguments[i]); + } + } // End-of: function logme +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js deleted file mode 100644 index 44674b96d3..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod1', [], function () { - console.log('we are in the mod1 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js deleted file mode 100644 index 9c26bb1dfe..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod2', [], function () { - console.log('we are in the mod2 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js deleted file mode 100644 index 0edf809155..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod4', [], function () { - console.log('we are in the mod4 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js new file mode 100644 index 0000000000..6ef53bdbeb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -0,0 +1,142 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Sliders', ['logme'], function (logme) { + return Sliders; + + function Sliders(gstId, config, state) { + logme('We are inside Sliders function.'); + + logme('gstId: ' + gstId); + logme(config); + logme(state); + + // We will go through all of the sliders. For each one, we will make a + // jQuery UI slider for it, attach "on change" events, and set it's + // state - initial value, max, and min parameters. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + createSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + createSlider(config.sliders.slider); + } + } + + function createSlider(obj) { + var constName, constValue, rangeBlobs, valueMin, valueMax, + sliderDiv, sliderWidth; + + // The name of the constant is obj['@var']. Multiple sliders and/or + // inputs can represent the same constant - therefore we will get + // the most recent const value from the state object. The range is + // a string composed of 3 blobs, separated by commas. The first + // blob is the min value for the slider, the third blob is the max + // value for the slider. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + if (typeof obj['@range'] !== 'string') { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + // Get the first blob from the split string. + valueMin = parseFloat(rangeBlobs[0]); + + if (isNaN(valueMin) === true) { + valueMin = constValue - 10; + } + + // Get the third blob from the split string. + valueMax = parseFloat(rangeBlobs[2]); + + if (isNaN(valueMax) === true) { + valueMax = constValue + 10; + } + + // Logically, the min, value, and max should make sense. + // I.e. we will make sure that: + // + // min <= value <= max + // + // If this is not the case, we will set some defaults. + if ((valueMin > valueMax) || + (valueMin > constValue) || + (valueMax < constValue)) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } + } + } + + sliderDiv = $('#' + gstId + '_slider_' + constName); + + // If a corresponding slider DIV for this constant does not exist, + // do not do anything. + if (sliderDiv.length === 0) { + return; + } + + // The default slider width. + sliderWidth = 400; + + logme('width: 0'); + logme(obj['@width']); + if (typeof obj['@width'] === 'string') { + logme('width: 1'); + if (isNaN(parseInt(obj['@width'], 10)) === false) { + logme('width: 2'); + sliderWidth = parseInt(obj['@width'], 10); + } + } + + // Set the new width to the slider. + sliderDiv.width(sliderWidth); + + // Create a jQuery UI slider from the current DIV. We will set + // starting parameters, and will also attach a handler to update + // the state on the change event. + sliderDiv.slider({ + 'min': valueMin, + 'max': valueMax, + 'value': constValue, + + 'change': sliderOnChange + }); + + return; + + function sliderOnChange(event, ui) { + state.setConstValue(constName, ui.value); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js new file mode 100644 index 0000000000..17c8721a73 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -0,0 +1,165 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('State', ['logme'], function (logme) { + // Since there will be (can be) multiple GST on a page, and each will have + // a separate state, we will create a factory constructor function. The + // constructor will expect the ID of the DIV with the GST contents, and the + // configuration object (parsed from a JSON string). It will return and + // object containing methods to set and get the private state properties. + + // This module defines and returns a factory constructor. + return State; + + /* + * function: State + * + * + */ + function State(gstId, config) { + var constants, c1; + + constants = {}; + + // We must go through all of the input, and slider elements and + // retrieve all of the available constants. These will be added to an + // object as it's properties. + // + // First we will go through all of the inputs. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + addConstFromInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + addConstFromInput(config.inputs.input); + } + } + + // Now we will go through all of the sliders. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + addConstFromSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + addConstFromSlider(config.sliders.slider); + } + } + + logme(constants); + + // The constructor will return an object with methods to operate on + // it's private properties. + return { + 'getConstValue': getConstValue, + 'setConstValue': setConstValue + }; + + function getConstValue(constName) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + return constants[constName]; + } + + function setConstValue(constName, constValue) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + if (isNaN(parseFloat(constValue)) === true) { + // We are interested only in valid float values. + return; + } + + constants[constName] = parseFloat(constValue); + + logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + } + + function addConstFromInput(obj) { + var constName, constValue; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is obj['@initial']. I have taken the word 'initial' + // into brackets, because multiple inputs and/or sliders can + // represent the state of a single constant. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@initial'] === 'undefined') { + constValue = 0; + } else { + constValue = parseFloat(obj['@initial']); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + + constants[constName] = constValue; + } + + function addConstFromSlider(obj) { + var constName, constValue, rangeBlobs; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is the second blob of the 'range' parameter of the + // slider which is obj['@range']. Multiple sliders and/or inputs + // can represent the same constant - therefore 'initial' is in + // brackets. The range is a string composed of 3 blobs, separated + // by commas. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@range'] !== 'string') { + constValue = 0; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + constValue = 0; + } else { + // Get the second blob from the split string. + constValue = parseFloat(rangeBlobs[1]); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + } + + constants[constName] = constValue; + } + } // End-of: function State +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index d6cffc67e2..17d2bae5e9 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,12 +1,14 @@
- -
+ + - -
+ + - + ${gst_html}
From ce7a01dd26b0f407487b4c78a27bad559b9699ae Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:08:36 +0200 Subject: [PATCH 236/329] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 2 +- .../js/src/graphical_slider_tool/graph.js | 62 +++++++++++++++++++ .../js/src/graphical_slider_tool/gst_main.js | 7 ++- .../js/src/graphical_slider_tool/mod5.js | 16 ----- .../js/src/graphical_slider_tool/state.js | 8 +++ 5 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 3d7b8a9f02..633f5e9406 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,7 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') ] diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js new file mode 100644 index 0000000000..2aa19cfc02 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -0,0 +1,62 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Graph', ['logme'], function (logme) { + + return Graph; + + function Graph(gstId, state) { + var plotDiv, data; + logme('We are inside Graph module.', gstId, state); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 0) { + return; + } + + plotDiv.width(300); + plotDiv.height(300); + + plotDiv.bind('update_plot', function (event, forGstId) { + if (forGstId !== gstId) { + logme('update_plot event not for current ID'); + } + + logme('redrawing plot'); + + generateData(); + updatePlot(); + }); + + generateData(); + updatePlot(); + + return; + + function generateData() { + var a, b, c1; + + a = state.getConstValue('a'); + b = state.getConstValue('b'); + + data = []; + data.push([]); + + for (c1 = 0; c1 < 30; c1++) { + data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + } + } + + function updatePlot() { + $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 9f2c4c356d..68ef73e441 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,7 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { +define( + 'GstMain', + ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, logme, GeneralMethods, Sliders, Graph) { logme(GeneralMethods); return GstMain; @@ -15,6 +18,8 @@ define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (Sta state = State(gstId, config); Sliders(gstId, config, state); + + Graph(gstId, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js deleted file mode 100644 index 5e843ac468..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod5', [], function () { - console.log('we are in the mod5 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 17c8721a73..ffd618c51b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -76,6 +76,8 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { + var plotDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -90,6 +92,12 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 1) { + plotDiv.trigger('update_plot', [gstId]); + } } function addConstFromInput(obj) { From 7de575a84bf5f1915b5a58c0e0646e8c7f6e99e7 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:26:29 +0200 Subject: [PATCH 237/329] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 18 ++++++++---------- .../js/src/graphical_slider_tool/state.js | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 2aa19cfc02..fbd1f96da1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -19,22 +19,20 @@ define('Graph', ['logme'], function (logme) { plotDiv.width(300); plotDiv.height(300); - plotDiv.bind('update_plot', function (event, forGstId) { - if (forGstId !== gstId) { - logme('update_plot event not for current ID'); - } - - logme('redrawing plot'); - - generateData(); - updatePlot(); - }); + state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); generateData(); updatePlot(); return; + function onUpdatePlot(event) { + logme('redrawing plot'); + + generateData(); + updatePlot(); + } + function generateData() { var a, b, c1; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index ffd618c51b..735c100344 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -18,7 +18,7 @@ define('State', ['logme'], function (logme) { * */ function State(gstId, config) { - var constants, c1; + var constants, c1, plotDiv; constants = {}; @@ -62,9 +62,16 @@ define('State', ['logme'], function (logme) { // it's private properties. return { 'getConstValue': getConstValue, - 'setConstValue': setConstValue + 'setConstValue': setConstValue, + 'bindUpdatePlotEvent': bindUpdatePlotEvent }; + function bindUpdatePlotEvent(newPlotDiv, callback) { + plotDiv = newPlotDiv; + + plotDiv.bind('update_plot', callback); + } + function getConstValue(constName) { if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an @@ -76,8 +83,6 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { - var plotDiv; - if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -93,10 +98,8 @@ define('State', ['logme'], function (logme) { logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - plotDiv = $('#' + gstId + '_plot'); - - if (plotDiv.length === 1) { - plotDiv.trigger('update_plot', [gstId]); + if (plotDiv !== undefined) { + plotDiv.trigger('update_plot'); } } From 32c70a524c3a3dca1cb1939b6417f9677ee8809b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 11:37:55 +0200 Subject: [PATCH 238/329] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 95 ++++++++++++++++--- .../js/src/graphical_slider_tool/gst_main.js | 7 +- .../js/src/graphical_slider_tool/sliders.js | 12 +-- .../js/src/graphical_slider_tool/state.js | 34 +++++-- 4 files changed, 113 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index fbd1f96da1..762789cbf5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,12 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', ['logme'], function (logme) { +define('Graph', [], function () { return Graph; - function Graph(gstId, state) { - var plotDiv, data; - logme('We are inside Graph module.', gstId, state); + function Graph(gstId, config, state) { + var plotDiv, dataSets, functions; plotDiv = $('#' + gstId + '_plot'); @@ -21,34 +20,102 @@ define('Graph', ['logme'], function (logme) { state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); + createFunctions(); + generateData(); updatePlot(); return; - function onUpdatePlot(event) { - logme('redrawing plot'); + function createFunctions() { + functions = []; + if (typeof config.plot['function'] === 'undefined') { + return; + } + + if (typeof config.plot['function'] === 'string') { + addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { + + } else if ($.isArray(config.plot['function'])) { + + } + + return; + + function addFunction(funcString, color, line, dot, label, style, point_size) { + var newFunctionObject, func, constNames; + + if (typeof funcString !== 'string') { + return; + } + + newFunctionObject = {}; + + constNames = state.getAllConstantNames(); + + // The 'x' is always one of the function parameters. + constNames.push('x'); + + // Must make sure that the function body also gets passed to + // the Function cosntructor. + constNames.push(funcString); + + func = Function.apply(null, constNames); + newFunctionObject['func'] = func; + + if (typeof color === 'string') { + newFunctionObject['color'] = color; + } + + if (typeof line === 'boolean') { + newFunctionObject['line'] = line; + } + + if (typeof dot === 'boolean') { + newFunctionObject['dot'] = dot; + } + + if (typeof label === 'string') { + newFunctionObject['label'] = label; + } + + functions.push(newFunctionObject); + } + } + + function onUpdatePlot(event) { generateData(); updatePlot(); } function generateData() { - var a, b, c1; + var c0, c1, datapoints, constValues, x, y; - a = state.getConstValue('a'); - b = state.getConstValue('b'); + constValues = state.getAllConstantValues(); - data = []; - data.push([]); + dataSets = []; - for (c1 = 0; c1 < 30; c1++) { - data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + for (c0 = 0; c0 < functions.length; c0 += 1) { + datapoints = []; + + for (c1 = 0; c1 < 30; c1 += 0.1) { + x = c1; + // Push the 'x' variable to the end of the parameter array. + constValues.push(x); + y = functions[c0].func.apply(window, constValues); + constValues.pop(); + + datapoints.push([x, y]); + } + + dataSets.push(datapoints); } } function updatePlot() { - $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + $.plot(plotDiv, dataSets); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 68ef73e441..71de12b423 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,9 +4,8 @@ define( 'GstMain', - ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, logme, GeneralMethods, Sliders, Graph) { - logme(GeneralMethods); + ['State', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, GeneralMethods, Sliders, Graph) { return GstMain; @@ -19,7 +18,7 @@ define( Sliders(gstId, config, state); - Graph(gstId, state); + Graph(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 6ef53bdbeb..e871e9f035 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -2,16 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Sliders', ['logme'], function (logme) { +define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { - logme('We are inside Sliders function.'); - - logme('gstId: ' + gstId); - logme(config); - logme(state); - // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. @@ -102,12 +96,8 @@ define('Sliders', ['logme'], function (logme) { // The default slider width. sliderWidth = 400; - logme('width: 0'); - logme(obj['@width']); if (typeof obj['@width'] === 'string') { - logme('width: 1'); if (isNaN(parseInt(obj['@width'], 10)) === false) { - logme('width: 2'); sliderWidth = parseInt(obj['@width'], 10); } } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 735c100344..d632429c9b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -2,7 +2,7 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('State', ['logme'], function (logme) { +define('State', [], function () { // Since there will be (can be) multiple GST on a page, and each will have // a separate state, we will create a factory constructor function. The // constructor will expect the ID of the DIV with the GST contents, and the @@ -56,16 +56,40 @@ define('State', ['logme'], function (logme) { } } - logme(constants); - // The constructor will return an object with methods to operate on // it's private properties. return { 'getConstValue': getConstValue, 'setConstValue': setConstValue, - 'bindUpdatePlotEvent': bindUpdatePlotEvent + 'bindUpdatePlotEvent': bindUpdatePlotEvent, + 'getAllConstantNames': getAllConstantNames, + 'getAllConstantValues': getAllConstantValues }; + function getAllConstantNames() { + var constName, allConstNames; + + allConstNames = []; + + for (constName in constants) { + allConstNames.push(constName); + } + + return allConstNames; + } + + function getAllConstantValues() { + var constName, allConstValues; + + allConstValues = []; + + for (constName in constants) { + allConstValues.push(constants[constName]); + } + + return allConstValues; + } + function bindUpdatePlotEvent(newPlotDiv, callback) { plotDiv = newPlotDiv; @@ -96,8 +120,6 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); - logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } From ae03090f3c980ce5b7c7ee5862f31acc86b08773 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 12:43:01 +0200 Subject: [PATCH 239/329] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 ++--- .../xmodule/xmodule/js/src/graphical_slider_tool/graph.js | 8 +++++++- .../xmodule/js/src/graphical_slider_tool/sliders.js | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 633f5e9406..c07c0670d7 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -129,8 +129,7 @@ class GraphicalSliderToolModule(XModule): """ #substitute plot plot_div = '
\ - This is plot
' + style="width: 600px; height: 600px; padding: 0px; position: relative;">This is plot
' html_string = html_string.replace('$plot$', plot_div) # substitute sliders @@ -140,7 +139,7 @@ class GraphicalSliderToolModule(XModule): vars = [x['@var'] for x in sliders] slider_div = '
This is slider
' + data-var="{var}">' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 762789cbf5..991cb0a26e 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -28,6 +28,8 @@ define('Graph', [], function () { return; function createFunctions() { + var c1; + functions = []; if (typeof config.plot['function'] === 'undefined') { @@ -39,7 +41,11 @@ define('Graph', [], function () { } else if ($.isPlainObject(config.plot['function']) === true) { } else if ($.isArray(config.plot['function'])) { - + for (c1 = 0; c1 < config.plot['function'].length; c1++) { + if (typeof config.plot['function'][c1] === 'string') { + addFunction(config.plot['function'][c1]); + } + } } return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index e871e9f035..226f53d696 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -112,6 +112,7 @@ define('Sliders', [], function () { 'min': valueMin, 'max': valueMax, 'value': constValue, + 'step': 0.01, 'change': sliderOnChange }); From 3e9d325a9f166901a2e24bc2dfe3942173f892c8 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 14:07:06 +0200 Subject: [PATCH 240/329] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 +- .../js/src/graphical_slider_tool/gst_main.js | 5 +- .../js/src/graphical_slider_tool/inputs.js | 70 +++++++++++++++++++ .../js/src/graphical_slider_tool/sliders.js | 1 + .../js/src/graphical_slider_tool/state.js | 7 ++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index c07c0670d7..9e9273bc25 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,6 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -154,8 +155,8 @@ class GraphicalSliderToolModule(XModule): inputs = [inputs] vars = [x['@var'] for x in inputs] - input_div = '
This is input
' + input_div = '' for var in vars: html_string = re.sub(r'\$input\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 71de12b423..47881b66c6 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,8 +4,8 @@ define( 'GstMain', - ['State', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, GeneralMethods, Sliders, Graph) { + ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], + function (State, GeneralMethods, Sliders, Inputs, Graph) { return GstMain; @@ -17,6 +17,7 @@ define( state = State(gstId, config); Sliders(gstId, config, state); + Inputs(gstId, config, state); Graph(gstId, config, state); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js new file mode 100644 index 0000000000..5b9f1f87c2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -0,0 +1,70 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Inputs', ['logme'], function (logme) { + return Inputs; + + function Inputs(gstId, config, state) { + logme('Inside "Inputs" module.'); + logme(gstId, config, state); + + // We will go thorugh all of the inputs, and those that have a valid + // '@var' property will be added to the page as a HTML text input + // element. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + createInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + createInput(config.inputs.input); + } + } + + function createInput(obj) { + var constName, constValue, inputDiv, textInputDiv; + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + inputDiv = $('#' + gstId + '_input_' + constName); + + if (inputDiv.length === 0) { + return; + } + + textInputDiv = $(''); + textInputDiv.width(50); + + textInputDiv.appendTo(inputDiv); + textInputDiv.val(constValue); + + textInputDiv.bind('change', inputOnChange); + + return; + + function inputOnChange(event) { + state.setConstValue(constName, $(this).val()); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 226f53d696..51bd2c8b12 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -104,6 +104,7 @@ define('Sliders', [], function () { // Set the new width to the slider. sliderDiv.width(sliderWidth); + sliderDiv.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index d632429c9b..88951f0e9d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -107,6 +107,8 @@ define('State', [], function () { } function setConstValue(constName, constValue) { + var inputDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -123,6 +125,11 @@ define('State', [], function () { if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } + + inputDiv = $('#' + gstId + '_input_' + constName).children('input'); + if (inputDiv.length !== 0) { + inputDiv.val(constValue); + } } function addConstFromInput(obj) { From b08b25b98388e4f42f501c930f8d7a4490fca960 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 16:11:29 +0200 Subject: [PATCH 241/329] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 4 +-- .../js/src/graphical_slider_tool/inputs.js | 27 ++++++++++++------- .../js/src/graphical_slider_tool/sliders.js | 20 ++++++++------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 9e9273bc25..61a883fbf8 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -139,8 +139,8 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
' + slider_div = '' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index 5b9f1f87c2..d7f64328e0 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -27,9 +27,9 @@ define('Inputs', ['logme'], function (logme) { } function createInput(obj) { - var constName, constValue, inputDiv, textInputDiv; + var constName, constValue, spanEl, inputEl; - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -40,19 +40,28 @@ define('Inputs', ['logme'], function (logme) { constValue = 0; } - inputDiv = $('#' + gstId + '_input_' + constName); + spanEl = $('#' + gstId + '_input_' + constName); - if (inputDiv.length === 0) { + if (spanEl.length === 0) { return; } - textInputDiv = $(''); - textInputDiv.width(50); + inputEl = $(''); - textInputDiv.appendTo(inputDiv); - textInputDiv.val(constValue); + // inputEl.width(50); + inputEl.val(constValue); + inputEl.bind('change', inputOnChange); + inputEl.button().css({ + 'font': 'inherit', + 'color': 'inherit', + 'text-align': 'left', + 'outline': 'none', + 'cursor': 'text', + 'height': '15px', + 'width': '50px' + }); - textInputDiv.bind('change', inputOnChange); + inputEl.appendTo(spanEl); return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 51bd2c8b12..3db0c3e67c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -25,7 +25,7 @@ define('Sliders', [], function () { function createSlider(obj) { var constName, constValue, rangeBlobs, valueMin, valueMax, - sliderDiv, sliderWidth; + spanEl, sliderEl, sliderWidth; // The name of the constant is obj['@var']. Multiple sliders and/or // inputs can represent the same constant - therefore we will get @@ -34,7 +34,7 @@ define('Sliders', [], function () { // blob is the min value for the slider, the third blob is the max // value for the slider. - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -85,14 +85,16 @@ define('Sliders', [], function () { } } - sliderDiv = $('#' + gstId + '_slider_' + constName); + spanEl = $('#' + gstId + '_slider_' + constName); // If a corresponding slider DIV for this constant does not exist, // do not do anything. - if (sliderDiv.length === 0) { + if (spanEl.length === 0) { return; } + sliderEl = $('
'); + // The default slider width. sliderWidth = 400; @@ -103,21 +105,23 @@ define('Sliders', [], function () { } // Set the new width to the slider. - sliderDiv.width(sliderWidth); - sliderDiv.css('display', 'inline-block'); + sliderEl.width(sliderWidth); + sliderEl.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update // the state on the change event. - sliderDiv.slider({ + sliderEl.slider({ 'min': valueMin, 'max': valueMax, 'value': constValue, - 'step': 0.01, + 'step': (valueMax - valueMin) / 50.0, 'change': sliderOnChange }); + sliderEl.appendTo(spanEl); + return; function sliderOnChange(event, ui) { From 3682cb46c4522bee93c6acafa7e38c02cc052403 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 18:37:20 +0200 Subject: [PATCH 242/329] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 15 +- .../js/src/graphical_slider_tool/inputs.js | 72 +++++++-- .../js/src/graphical_slider_tool/sliders.js | 137 ++++++++++++++---- 3 files changed, 186 insertions(+), 38 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 991cb0a26e..61228413f5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -121,7 +121,20 @@ define('Graph', [], function () { } function updatePlot() { - $.plot(plotDiv, dataSets); + $.plot( + plotDiv, + dataSets, + { + 'xaxis': { + 'min': 0, + 'max': 30 + }, + 'yaxis': { + 'min': -5, + 'max': 5 + } + } + ); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index d7f64328e0..3e7e55f02c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -2,12 +2,24 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Inputs', ['logme'], function (logme) { +define('Inputs', [], function () { return Inputs; function Inputs(gstId, config, state) { - logme('Inside "Inputs" module.'); - logme(gstId, config, state); + var constNamesUsed; + + // There should not be more than one text input per a constant. This + // just does not make sense. However, nothing really prevents the user + // from specifying more than one text input for the same constant name. + // That's why we have to track which constant names already have + // text inputs for them, and prevent adding further text inputs to + // these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a text input to. + // When creating a new text input, we must consult with this object, to + // see if the constant name is not defined as it's property. + constNamesUsed = {}; // We will go thorugh all of the inputs, and those that have a valid // '@var' property will be added to the page as a HTML text input @@ -15,42 +27,75 @@ define('Inputs', ['logme'], function (logme) { if ((typeof config.inputs !== 'undefined') && (typeof config.inputs.input !== 'undefined')) { if ($.isArray(config.inputs.input)) { - // config.inputs.input is an array + // config.inputs.input is an array. For each element, we will + // add a text input. for (c1 = 0; c1 < config.inputs.input.length; c1++) { createInput(config.inputs.input[c1]); } } else if ($.isPlainObject(config.inputs.input)) { - // config.inputs.input is an object + + // config.inputs.input is an object. Add a text input for it. createInput(config.inputs.input); + } } function createInput(obj) { var constName, constValue, spanEl, inputEl; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a text input for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a text input for a constant which already has a + // text input defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, we will skip creating a text input + // for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // With the constant name, and the constant value being defined, + // lets get the element on the page into which the text input will + // be inserted. spanEl = $('#' + gstId + '_input_' + constName); + // If a corresponding element for this constant does not exist on + // the page, we will not be making a text input. if (spanEl.length === 0) { return; } + // Create the text input element. inputEl = $(''); - // inputEl.width(50); + // Set the current constant to the text input. It will be visible + // to the user. inputEl.val(constValue); + + // Bind a function to the 'change' event. Whenever the user changes + // the value of this text input, and presses 'enter' (or clicks + // somewhere else on the page), this event will be triggered, and + // our callback will be called. inputEl.bind('change', inputOnChange); + + // Lets style the input element nicely. We will use the button() + // widget for this since there is no native widget for the text + // input. inputEl.button().css({ 'font': 'inherit', 'color': 'inherit', @@ -61,10 +106,17 @@ define('Inputs', ['logme'], function (logme) { 'width': '50px' }); + // And finally, publish the text input element to the page. inputEl.appendTo(spanEl); + // Don't forget to add the constant to the list of used constants. + // Next time a slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // When the user changes the value of this text input, the 'state' + // will be updated, forcing the plot to be redrawn. function inputOnChange(event) { state.setConstValue(constName, $(this).val()); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 3db0c3e67c..33bdd89dd1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -6,68 +6,117 @@ define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { + var constNamesUsed; + + // There should not be more than one slider per a constant. This just + // does not make sense. However, nothing really prevents the user from + // specifying more than one slider for the same constant name. That's + // why we have to track which constant names already have sliders for + // them, and prevent adding further sliders to these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a slider to. When + // creating a new slider, we must consult with this object, to see if + // the constant name is not defined as it's property. + constNamesUsed = {}; + // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. if ((typeof config.sliders !== 'undefined') && (typeof config.sliders.slider !== 'undefined')) { if ($.isArray(config.sliders.slider)) { - // config.sliders.slider is an array + // config.sliders.slider is an array. For each object in the + // array, create a slider. for (c1 = 0; c1 < config.sliders.slider.length; c1++) { createSlider(config.sliders.slider[c1]); } + } else if ($.isPlainObject(config.sliders.slider)) { - // config.sliders.slider is an object + + // config.sliders.slider is an object. Create a slider for it. createSlider(config.sliders.slider); + } } function createSlider(obj) { - var constName, constValue, rangeBlobs, valueMin, valueMax, - spanEl, sliderEl, sliderWidth; - - // The name of the constant is obj['@var']. Multiple sliders and/or - // inputs can represent the same constant - therefore we will get - // the most recent const value from the state object. The range is - // a string composed of 3 blobs, separated by commas. The first - // blob is the min value for the slider, the third blob is the max - // value for the slider. + var constName, constValue, rangeBlobs, valueMin, valueMax, spanEl, + sliderEl, sliderWidth; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a slider for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a slider for a constant which already has a + // slider defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, then something terrible has + // happened! We will skip creating a slider for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // The range is a string composed of 3 blobs, separated by commas. + // The first blob is the min value for the slider, the third blob + // is the max value for the slider. if (typeof obj['@range'] !== 'string') { + + // If the range is not a string, we will set a default range. + // No promise as to the quality of the data points that this + // range will produce. valueMin = constValue - 10; valueMax = constValue + 10; + } else { + + // Separate the range string by commas, and store each blob as + // an element in an array. rangeBlobs = obj['@range'].split(','); // We must have gotten exactly 3 blobs (pieces) from the split. if (rangeBlobs.length !== 3) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if the range string was + // split into more or less than 3 pieces. + setDefaultMinMax(); + } else { - // Get the first blob from the split string. + + // Get the first blob from the split string. It is the min + // value. valueMin = parseFloat(rangeBlobs[0]); + // Is it a well-formed float number? if (isNaN(valueMin) === true) { + + // No? Then set a sensible default value. valueMin = constValue - 10; + } - // Get the third blob from the split string. + // Get the third blob from the split string. It is the max. valueMax = parseFloat(rangeBlobs[2]); + // Is it a well-formed float number? if (isNaN(valueMax) === true) { + + // No? Then set a sensible default value. valueMax = constValue + 10; + } // Logically, the min, value, and max should make sense. @@ -79,38 +128,53 @@ define('Sliders', [], function () { if ((valueMin > valueMax) || (valueMin > constValue) || (valueMax < constValue)) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if min/value/max logic + // is broken. + setDefaultMinMax(); + } } } + // At this point we have the constant name, the constant value, and + // the min and max values for this slider. Lets get the element on + // the page into which the slider will be inserted. spanEl = $('#' + gstId + '_slider_' + constName); - // If a corresponding slider DIV for this constant does not exist, - // do not do anything. + // If a corresponding element for this constant does not exist on + // the page, we will not be making a slider. if (spanEl.length === 0) { return; } + // Create the slider DIV. sliderEl = $('
'); - // The default slider width. + // We will define the width of the slider to a sensible default. sliderWidth = 400; + // Then we will see if one is provided in the config for this + // slider. If we find it, and it is a well-formed integer, we will + // use it, instead of the default width. if (typeof obj['@width'] === 'string') { if (isNaN(parseInt(obj['@width'], 10)) === false) { sliderWidth = parseInt(obj['@width'], 10); } } - // Set the new width to the slider. + // Set the defined width to the slider. sliderEl.width(sliderWidth); + + // And make sure that it gets added to the page as an + // 'inline-block' element. This will allow for the insertion of the + // slider into a paragraph, without the browser forcing it out of + // the paragraph onto a new line, separate line. sliderEl.css('display', 'inline-block'); - // Create a jQuery UI slider from the current DIV. We will set + // Create a jQuery UI slider from the slider DIV. We will set // starting parameters, and will also attach a handler to update - // the state on the change event. + // the 'state' on the 'change' event. sliderEl.slider({ 'min': valueMin, 'max': valueMax, @@ -120,13 +184,32 @@ define('Sliders', [], function () { 'change': sliderOnChange }); + // Append the slider DIV to the element on the page where the user + // wants to see it. sliderEl.appendTo(spanEl); + // OK! So we made it this far... + // + // Adding the constant to the list of used constants. Next time a + // slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // Update the 'state' - i.e. set the value of the constant this + // slider is attached to to a new value. + // + // This will cause the plot to be redrawn each time after the user + // drags the slider handle and releases it. function sliderOnChange(event, ui) { state.setConstValue(constName, ui.value); } + + // The sensible defaults for the slider's range. + function setDefaultMinMax() { + valueMin = constValue - 10; + valueMax = constValue + 10; + } } } }); From 28f4921924c1e47c7a68a754292a63dcdea6c35e Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 06:56:43 +0200 Subject: [PATCH 243/329] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 61228413f5..dba0483674 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,15 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', [], function () { +define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { var plotDiv, dataSets, functions; + logme(config); + plotDiv = $('#' + gstId + '_plot'); if (plotDiv.length === 0) { @@ -39,11 +41,29 @@ define('Graph', [], function () { if (typeof config.plot['function'] === 'string') { addFunction(config.plot['function']); } else if ($.isPlainObject(config.plot['function']) === true) { - + addFunction( + config.plot['function']['#text'], + config.plot['function']['@color'], + config.plot['function']['@dot'], + config.plot['function']['@label'], + config.plot['function']['@line'], + config.plot['function']['@point_size'], + config.plot['function']['@style'] + ); } else if ($.isArray(config.plot['function'])) { for (c1 = 0; c1 < config.plot['function'].length; c1++) { if (typeof config.plot['function'][c1] === 'string') { addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { + addFunction( + config.plot['function'][c1]['#text'], + config.plot['function'][c1]['@color'], + config.plot['function'][c1]['@dot'], + config.plot['function'][c1]['@label'], + config.plot['function'][c1]['@line'], + config.plot['function'][c1]['@point_size'], + config.plot['function'][c1]['@style'] + ); } } } @@ -76,17 +96,31 @@ define('Graph', [], function () { } if (typeof line === 'boolean') { - newFunctionObject['line'] = line; + if ((line === 'true') || (line === true)) { + newFunctionObject['line'] = true; + } else { + newFunctionObject['line'] = false; + } } - if (typeof dot === 'boolean') { - newFunctionObject['dot'] = dot; + if ((typeof dot === 'boolean') || (typeof dot === 'string')) { + if ((dot === 'true') || (dot === true)) { + newFunctionObject['dot'] = true; + } else { + newFunctionObject['dot'] = false; + } + } + + if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { + newFunctionObject['line'] = true; } if (typeof label === 'string') { newFunctionObject['label'] = label; } + logme(newFunctionObject); + functions.push(newFunctionObject); } } From 3e46ecef646f194798dff81dc4f4176a9e69a27f Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 10:47:46 +0200 Subject: [PATCH 244/329] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 132 ++++++++++++++---- .../js/src/graphical_slider_tool/gst_main.js | 4 + .../js/graphical_slider_tool/gst_module.js | 15 -- .../static/js/graphical_slider_tool/main.js | 75 ---------- 4 files changed, 108 insertions(+), 118 deletions(-) delete mode 100644 common/static/js/graphical_slider_tool/gst_module.js delete mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index dba0483674..c0c8addf80 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -7,7 +7,7 @@ define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { - var plotDiv, dataSets, functions; + var plotDiv, dataSeries, functions; logme(config); @@ -39,37 +39,55 @@ define('Graph', ['logme'], function (logme) { } if (typeof config.plot['function'] === 'string') { + + // If just one function string is present. addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { - addFunction( - config.plot['function']['#text'], - config.plot['function']['@color'], - config.plot['function']['@dot'], - config.plot['function']['@label'], - config.plot['function']['@line'], - config.plot['function']['@point_size'], - config.plot['function']['@style'] - ); + + // If a function is present, but it also has properties + // defined. + callAddFunction(config.plot['function']); + } else if ($.isArray(config.plot['function'])) { + + // If more than one function is defined. for (c1 = 0; c1 < config.plot['function'].length; c1++) { + + // For each definition, we must check if it is a simple + // string definition, or a complex one with properties. if (typeof config.plot['function'][c1] === 'string') { + + // Simple string. addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { - addFunction( - config.plot['function'][c1]['#text'], - config.plot['function'][c1]['@color'], - config.plot['function'][c1]['@dot'], - config.plot['function'][c1]['@label'], - config.plot['function'][c1]['@line'], - config.plot['function'][c1]['@point_size'], - config.plot['function'][c1]['@style'] - ); + + // Properties are present. + callAddFunction(config.plot['function'][c1]); + } } } return; + // This function will reduce code duplications. We have to call + // the function addFunction() several times passing object + // properties. A parameters. Rather than writing them out every + // time, we will have a single point of + function callAddFunction(obj) { + addFunction( + obj['#text'], + obj['@color'], + obj['@line'], + obj['@dot'], + obj['@label'], + obj['@style'], + obj['@point_size'] + ); + } + function addFunction(funcString, color, line, dot, label, style, point_size) { var newFunctionObject, func, constNames; @@ -95,7 +113,7 @@ define('Graph', ['logme'], function (logme) { newFunctionObject['color'] = color; } - if (typeof line === 'boolean') { + if ((typeof line === 'boolean') || (typeof line === 'string')) { if ((line === 'true') || (line === true)) { newFunctionObject['line'] = true; } else { @@ -111,6 +129,9 @@ define('Graph', ['logme'], function (logme) { } } + // By default, if no preference was set, or if the preference + // is conflicting (we must have either line or dot, none is + // not an option), we will show line. if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { newFunctionObject['line'] = true; } @@ -131,33 +152,68 @@ define('Graph', ['logme'], function (logme) { } function generateData() { - var c0, c1, datapoints, constValues, x, y; + var c0, c1, functionObj, seriesObj, dataPoints, constValues, x, y; constValues = state.getAllConstantValues(); - dataSets = []; + dataSeries = []; for (c0 = 0; c0 < functions.length; c0 += 1) { - datapoints = []; + functionObj = functions[c0]; + logme('Functions obj:', functionObj); - for (c1 = 0; c1 < 30; c1 += 0.1) { + seriesObj = {}; + dataPoints = []; + + for (c1 = 0; c1 < 30; c1 += 1) { x = c1; + // Push the 'x' variable to the end of the parameter array. constValues.push(x); - y = functions[c0].func.apply(window, constValues); + + // We call the user defined function, passing all of the + // available constant values. inside this function they + // will be accessible by their names. + y = functionObj.func.apply(window, constValues); + + // Return the constValues array to how it was before we + // added 'x' variable to the end of it. constValues.pop(); - datapoints.push([x, y]); + // Add the generated point to the data points set. + dataPoints.push([x, y]); + } - dataSets.push(datapoints); + // Put the entire data points set into the series object. + seriesObj.data = dataPoints; + + // See if user defined a specific color for this function. + if (functionObj.hasOwnProperty('color') === true) { + seriesObj.color = functionObj.color; + } + + // See if a user defined a label for this function. + if (functionObj.hasOwnProperty('label') === true) { + seriesObj.label = functionObj.label; + } + + seriesObj.lines = { + 'show': functionObj.line + }; + + seriesObj.points = { + 'show': functionObj.dot + }; + + dataSeries.push(seriesObj); } } function updatePlot() { $.plot( plotDiv, - dataSets, + dataSeries, { 'xaxis': { 'min': 0, @@ -166,9 +222,29 @@ define('Graph', ['logme'], function (logme) { 'yaxis': { 'min': -5, 'max': 5 + }, + 'legend': { + + // To show the legend or not. Note, even if 'show' is + // 'true', the legend will only show if labels are + // provided for at least one of the series that are + // going to be plotted. + 'show': true, + + // A floating point number in the range [0, 1]. The + // smaller the number, the more transparent will the + // legend background become. + 'backgroundOpacity': 0 + } } ); + + MathJax.Hub.Queue([ + 'Typeset', + MathJax.Hub, + plotDiv.attr('id') + ]); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 47881b66c6..8611fed1f2 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,6 +4,10 @@ define( 'GstMain', + + // Even though it is not explicitly in this module, we have to specify + // 'GeneralMethods' as a dependency. It expands some of the core JS objects + // with additional useful methods that are used in other modules. ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], function (State, GeneralMethods, Sliders, Inputs, Graph) { diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js deleted file mode 100644 index c4661b5e44..0000000000 --- a/common/static/js/graphical_slider_tool/gst_module.js +++ /dev/null @@ -1,15 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define([], function () { - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js deleted file mode 100644 index da36d9c9d6..0000000000 --- a/common/static/js/graphical_slider_tool/main.js +++ /dev/null @@ -1,75 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -// For documentation please check: -// http://requirejs.org/docs/api.html -requirejs.config({ - // Because require.js is included as a simple