From 8e7eef24a2e1a1eda91787d1c22e3053cd7b6500 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 23 Aug 2012 13:10:08 -0400 Subject: [PATCH 01/90] add manage_course_groups command to lms_migration --- .../commands/manage_course_groups.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lms/djangoapps/lms_migration/management/commands/manage_course_groups.py diff --git a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py new file mode 100644 index 0000000000..0043b483e0 --- /dev/null +++ b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# +# File: manage_course_groups +# +# interactively list and edit membership in course staff and instructor groups + +import os, sys, string, re +import datetime +from getpass import getpass +import json +import readline + +from django.core.management.base import BaseCommand +from django.conf import settings +from django.contrib.auth.models import User, Group + +#----------------------------------------------------------------------------- +# get all staff groups + +class Command(BaseCommand): + help = "Manage course group membership, interactively." + + def handle(self, *args, **options): + + gset = Group.objects.all() + + print "Groups:" + for cnt,g in zip(range(len(gset)), gset): + print "%d. %s" % (cnt,g) + + gnum = int(raw_input('Choose group to manage (enter #): ')) + + group = gset[gnum] + + #----------------------------------------------------------------------------- + # users in group + + uall = User.objects.all() + print "----" + print "List of All Users: %s" % [str(x.username) for x in uall] + print "----" + + while True: + + print "Users in the group:" + + uset = group.user_set.all() + for cnt, u in zip(range(len(uset)), uset): + print "%d. %s" % (cnt, u) + + action = raw_input('Choose user to delete (enter #) or enter usernames (comma delim) to add: ') + + m = re.match('^[0-9]+$',action) + if m: + unum = int(action) + u = uset[unum] + print "Deleting user %s" % u + u.groups.remove(group) + + else: + for uname in action.split(','): + try: + user = User.objects.get(username=action) + except Exception as err: + print "Error %s" % err + continue + print "adding %s to group %s" % (user, group) + user.groups.add(group) + + + From d94ef5454999fe738cefbf9555515f23b70ed692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 23 Aug 2012 16:30:58 -0400 Subject: [PATCH 02/90] Added cheatsheet popup to wiki editor. --- lms/static/sass/course/wiki/_wiki.scss | 23 ++++++++- lms/templates/wiki/base.html | 27 +++++++---- lms/templates/wiki/includes/cheatsheet.html | 53 +++++++++++++++++++++ 3 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 lms/templates/wiki/includes/cheatsheet.html diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss index bcc2c8855d..ead58bf6a6 100644 --- a/lms/static/sass/course/wiki/_wiki.scss +++ b/lms/static/sass/course/wiki/_wiki.scss @@ -567,11 +567,30 @@ section.wiki { background: #f00 !important; } - - + #cheatsheetLink { + text-align:right; + display: float; + } + #cheatsheetModal { + width: 350px; + margin-left: 100px; + margin-top: -100px; + } + #cheatsheet-body { + background: #FFF; + text-align: left; + padding: 10px; + } + #cheatsheet-body pre{ + color: #000; + text-align: left; + background: #EEE; + margin:10px; + padding: 10px; + } /*----------------- diff --git a/lms/templates/wiki/base.html b/lms/templates/wiki/base.html index fc4a2d18d4..a346be0e3e 100644 --- a/lms/templates/wiki/base.html +++ b/lms/templates/wiki/base.html @@ -27,21 +27,29 @@ }); } - - + + {% addtoblock 'js' %} {% comment %} These scripts load at the bottom of the body {% endcomment %} - + - + {% with mathjax_mode='wiki' %} {% include "mathjax_include.html" %} {% endwith %} - + {% endaddtoblock %} - + {% endblock %} @@ -64,11 +72,14 @@ {% endfor %} {% endif %} - + {% block wiki_contents %}{% endblock %} - + {% endblock %} + + {% include "wiki/includes/cheatsheet.html" %} + {% endblock %} diff --git a/lms/templates/wiki/includes/cheatsheet.html b/lms/templates/wiki/includes/cheatsheet.html new file mode 100644 index 0000000000..d58920b814 --- /dev/null +++ b/lms/templates/wiki/includes/cheatsheet.html @@ -0,0 +1,53 @@ + From 77f928d118a3c718dd583ec4bf63bef9fad19140 Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 23 Aug 2012 17:24:59 -0400 Subject: [PATCH 03/90] Queuestate records both secret key and time of queue request --- common/lib/capa/capa/capa_problem.py | 6 +----- common/lib/capa/capa/correctmap.py | 11 ++++++----- common/lib/capa/capa/responsetypes.py | 14 ++++++++------ common/lib/xmodule/xmodule/capa_module.py | 6 ++++++ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 82eb330174..35c8eaf635 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -202,11 +202,7 @@ class LoncapaProblem(object): ''' Returns True if any part of the problem has been submitted to an external queue ''' - queued = False - for answer_id in self.correct_map: - if self.correct_map.is_queued(answer_id): - queued = True - return queued + return any([self.correct_map.is_queued(answer_id) for answer_id in self.correct_map]) def grade_answers(self, answers): ''' diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index eb6ef2d00c..c6fc98e62f 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -15,7 +15,8 @@ class CorrectMap(object): - msg : string (may have HTML) giving extra message response (displayed below textline or textbox) - hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) - hintmode : one of (None,'on_request','always') criteria for displaying hint - - queuekey : a random integer for xqueue_callback verification + - queuestate : Tuple (key, time) where key is a secret string, and time is a string dump + of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued Behaves as a dict. ''' @@ -31,14 +32,14 @@ class CorrectMap(object): def __iter__(self): return self.cmap.__iter__() - def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuekey=None): + def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None): if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, 'npoints': npoints, 'msg': msg, 'hint': hint, 'hintmode': hintmode, - 'queuekey': queuekey, + 'queuestate': queuestate, } def __repr__(self): @@ -67,10 +68,10 @@ class CorrectMap(object): return None def is_queued(self, answer_id): - return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] is not None + return answer_id in self.cmap and self.cmap[answer_id]['queuestate'] is not None def is_right_queuekey(self, answer_id, test_key): - return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] == test_key + return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate'][0] == test_key def get_npoints(self, answer_id): npoints = self.get_property(answer_id, 'npoints') diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b2d56b48ca..35b8688a7b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -26,6 +26,7 @@ import xml.sax.saxutils as saxutils # specific library imports from calc import evaluator, UndefinedVariable from correctmap import CorrectMap +from datetime import datetime from util import * from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? @@ -1026,7 +1027,7 @@ class CodeResponse(LoncapaResponse): TODO: Determines whether in synchronous or asynchronous (queued) mode ''' xml = self.xml - self.url = xml.get('url', None) # XML can override external resource (grader/queue) URL + self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) # VS[compat]: @@ -1128,7 +1129,7 @@ class CodeResponse(LoncapaResponse): xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) - + # Generate body if is_list_of_files(submission): self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here @@ -1148,16 +1149,17 @@ class CodeResponse(LoncapaResponse): (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) + queuestate = (queuekey,'') cmap = CorrectMap() if error: - cmap.set(self.answer_id, queuekey=None, + cmap.set(self.answer_id, queuestate=None, msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) else: # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuekey'] indicates that the problem has been queued + # 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 to poll the LMS - cmap.set(self.answer_id, queuekey=queuekey, correctness='incomplete', msg=msg) + cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) return cmap @@ -1180,7 +1182,7 @@ class CodeResponse(LoncapaResponse): points = 0 elif points > self.maxpoints[self.answer_id]: points = self.maxpoints[self.answer_id] - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuekey=None) # Queuekey is consumed + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed else: log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d2ed3912a4..6b6c0991c5 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -462,6 +462,12 @@ class CapaModule(XModule): self.system.track_function('save_problem_check_fail', event_info) raise NotFoundError('Problem must be reset before it can be checked again') + # Problem queued. Student should not be able to submit + ''' + if self.lcp.is_queued(): + return {'success': False, 'html': 'Already queued'} + ''' + try: old_state = self.lcp.get_state() lcp_id = self.lcp.problem_id From 673cc2bbb393b3cff6d3f7c2e5e8cbd3837bd73f Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 23 Aug 2012 17:43:38 -0400 Subject: [PATCH 04/90] Keep queueuing time --- common/lib/capa/capa/correctmap.py | 3 +++ common/lib/capa/capa/responsetypes.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index c6fc98e62f..4659e2d1e2 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -73,6 +73,9 @@ class CorrectMap(object): def is_right_queuekey(self, answer_id, test_key): return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate'][0] == test_key + def get_queuetime_str(self, answer_id): + return self.cmap[answer_id]['queuestate'][1] if self.is_queued(answer_id) else None + def get_npoints(self, answer_id): npoints = self.get_property(answer_id, 'npoints') if npoints is not None: diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 35b8688a7b..27801ad871 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1149,7 +1149,10 @@ class CodeResponse(LoncapaResponse): (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) - queuestate = (queuekey,'') + # State associated with the queueing request + qtime = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S') + queuestate = (queuekey, qtime) + cmap = CorrectMap() if error: cmap.set(self.answer_id, queuestate=None, From 95a7a5b08b51455463c6400666efe5acf59c7316 Mon Sep 17 00:00:00 2001 From: kimth Date: Thu, 23 Aug 2012 18:39:57 -0400 Subject: [PATCH 05/90] Student must wait XQUEUE_WAITTIME_BETWEEN_REQUESTS between queueing submissions --- common/lib/capa/capa/capa_problem.py | 16 ++++++++++++++++ common/lib/xmodule/xmodule/capa_module.py | 13 ++++++++----- lms/envs/dev.py | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 35c8eaf635..ee24ebd031 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -29,6 +29,7 @@ from xml.sax.saxutils import unescape import calc from correctmap import CorrectMap +from datetime import datetime import eia import inputtypes from util import contextualize_text, convert_files_to_filenames @@ -204,6 +205,21 @@ class LoncapaProblem(object): ''' return any([self.correct_map.is_queued(answer_id) for answer_id in self.correct_map]) + + def get_recentmost_queuetime(self): + ''' + Returns a DateTime object that represents the timestamp of the most recent queueing request, or None if not queued + ''' + if not self.is_queued(): + return None + + # Get a list of timestamps of all queueing requests, then convert it to a DateTime object + queuetimes = [self.correct_map.get_queuetime_str(answer_id) for answer_id in self.correct_map if self.correct_map.is_queued(answer_id)] + queuetimes = [datetime.strptime(qt,'%Y%m%d%H%M%S') for qt in queuetimes] + + return max(queuetimes) + + def grade_answers(self, answers): ''' Grade student responses. Called by capa_module.check_problem. diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 6b6c0991c5..8f570bcf6c 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -7,7 +7,8 @@ import traceback import re import sys -from datetime import timedelta +from datetime import datetime, timedelta +from django.conf import settings from lxml import etree from pkg_resources import resource_string @@ -462,11 +463,13 @@ class CapaModule(XModule): self.system.track_function('save_problem_check_fail', event_info) raise NotFoundError('Problem must be reset before it can be checked again') - # Problem queued. Student should not be able to submit - ''' + # Problem queued. Students must wait XQUEUE_WAITTIME_BETWEEN_REQUESTS if self.lcp.is_queued(): - return {'success': False, 'html': 'Already queued'} - ''' + current_time = datetime.now() + prev_submit_time = self.lcp.get_recentmost_queuetime() + if (current_time-prev_submit_time).total_seconds() < settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS: + msg = 'You must wait %d seconds between queueing requests' % settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS + return {'success': msg, 'html': ''} try: old_state = self.lcp.get_state() diff --git a/lms/envs/dev.py b/lms/envs/dev.py index a76e6de262..fe12ee3322 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -63,6 +63,7 @@ XQUEUE_INTERFACE = { }, "basic_auth": ('anant', 'agarwal'), } +XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds # Make the keyedcache startup warnings go away CACHE_TIMEOUT = 0 From fdecaef7eff4e4b4e83cf7ae840c80100bad1370 Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 24 Aug 2012 08:23:46 -0400 Subject: [PATCH 06/90] Fix datetime name conflict --- common/lib/xmodule/xmodule/capa_module.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 8f570bcf6c..33a2f6b4c4 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -7,7 +7,7 @@ import traceback import re import sys -from datetime import datetime, timedelta +from datetime import timedelta from django.conf import settings from lxml import etree from pkg_resources import resource_string @@ -465,10 +465,11 @@ class CapaModule(XModule): # Problem queued. Students must wait XQUEUE_WAITTIME_BETWEEN_REQUESTS if self.lcp.is_queued(): - current_time = datetime.now() + current_time = datetime.datetime.now() prev_submit_time = self.lcp.get_recentmost_queuetime() - if (current_time-prev_submit_time).total_seconds() < settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS: - msg = 'You must wait %d seconds between queueing requests' % settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS + waittime_between_requests = settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS + if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: + msg = 'You must wait at least %d seconds between queue submissions' % waittime_between_requests return {'success': msg, 'html': ''} try: From 97ab53e786c03453a69aeaa1cd7e4462ae37a9dd Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 24 Aug 2012 08:28:47 -0400 Subject: [PATCH 07/90] Queuekey --> Queuestate in tests --- common/lib/xmodule/xmodule/tests/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 2a380bb8be..5d70056d2a 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -292,7 +292,9 @@ class CodeResponseTest(unittest.TestCase): answer_ids = sorted(test_lcp.get_question_answers().keys()) numAnswers = len(answer_ids) for i in range(numAnswers): - old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000 + i)) + queuekey = 1000 + i + queuestate = (queuekey, '') + old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) # TODO: Message format inherited from ExternalResponse #correct_score_msg = "EXACT_ANSMESSAGE" @@ -326,7 +328,7 @@ class CodeResponseTest(unittest.TestCase): new_cmap = CorrectMap() new_cmap.update(old_cmap) npoints = 1 if correctness=='correct' else 0 - new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuekey=None) + new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) From 088d204d9cbd0d757f66f25be982ba4ba1d8ca3a Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 24 Aug 2012 11:09:27 -0400 Subject: [PATCH 08/90] Added CodeResponse tests --- common/lib/capa/capa/responsetypes.py | 2 +- common/lib/xmodule/xmodule/tests/__init__.py | 100 ++++++++++++++--- .../xmodule/tests/test_files/coderesponse.xml | 90 ++-------------- .../coderesponse_externalresponseformat.xml | 101 ++++++++++++++++++ lms/envs/test.py | 2 +- 5 files changed, 199 insertions(+), 96 deletions(-) create mode 100644 common/lib/xmodule/xmodule/tests/test_files/coderesponse_externalresponseformat.xml diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 27801ad871..5cf45adce2 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1202,7 +1202,7 @@ class CodeResponse(LoncapaResponse): ''' Grader reply is a JSON-dump of the following dict { 'correct': True/False, - 'score': # TODO -- Partial grading + 'score': Numeric value (floating point is okay) to assign to answer 'msg': grader_msg } Returns (valid_score_msg, correct, score, msg): diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 5d70056d2a..bd5d35c75f 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -19,6 +19,7 @@ import capa.calc as calc import capa.capa_problem as lcp from capa.correctmap import CorrectMap from capa.util import convert_files_to_filenames +from datetime import datetime from xmodule import graders, x_module from xmodule.x_module import ModuleSystem from xmodule.graders import Score, aggregate_scores @@ -283,30 +284,61 @@ class CodeResponseTest(unittest.TestCase): ''' Test CodeResponse ''' + @staticmethod + def make_queuestate(key, time): + timestr = datetime.strftime(time,'%Y%m%d%H%M%S') + return (key, timestr) + + def test_is_queued(self): + ''' + Simple test of whether LoncapaProblem knows when it's been queued + ''' + problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) + + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) + + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for i in range(num_answers): + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + test_lcp.correct_map.update(cmap) + + self.assertEquals(test_lcp.is_queued(), False) + + # Now we queue the LCP + cmap = CorrectMap() + for i in range(num_answers): + queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + test_lcp.correct_map.update(cmap) + + self.assertEquals(test_lcp.is_queued(), True) + def test_update_score(self): + ''' + Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem + ''' problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) - # CodeResponse requires internal CorrectMap state. Build it now in the 'queued' state - old_cmap = CorrectMap() answer_ids = sorted(test_lcp.get_question_answers().keys()) - numAnswers = len(answer_ids) - for i in range(numAnswers): + num_answers = len(answer_ids) + + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + old_cmap = CorrectMap() + for i in range(num_answers): queuekey = 1000 + i - queuestate = (queuekey, '') + queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now()) old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - # TODO: Message format inherited from ExternalResponse - #correct_score_msg = "EXACT_ANSMESSAGE" - #incorrect_score_msg = "WRONG_FORMATMESSAGE" - - # New message format common to external graders + # Message format common to external graders correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'}) incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'}) xserver_msgs = {'correct': correct_score_msg, - 'incorrect': incorrect_score_msg, - } + 'incorrect': incorrect_score_msg,} # Incorrect queuekey, state should not be updated for correctness in ['correct', 'incorrect']: @@ -316,12 +348,12 @@ class CodeResponseTest(unittest.TestCase): test_lcp.update_score(xserver_msgs[correctness], queuekey=0) self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison - for i in range(numAnswers): + for i in range(num_answers): self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered # Correct queuekey, state should be updated for correctness in ['correct', 'incorrect']: - for i in range(numAnswers): # Target specific answer_id's + for i in range(num_answers): # Target specific answer_id's test_lcp.correct_map = CorrectMap() test_lcp.correct_map.update(old_cmap) @@ -333,13 +365,51 @@ class CodeResponseTest(unittest.TestCase): test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) - for j in range(numAnswers): + for j in range(num_answers): if j == i: self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered else: self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered + + + def test_recentmost_queuetime(self): + ''' + Test whether the LoncapaProblem knows about the time of queue requests + ''' + problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) + + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) + + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for i in range(num_answers): + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + test_lcp.correct_map.update(cmap) + + self.assertEquals(test_lcp.get_recentmost_queuetime(), None) + + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + cmap = CorrectMap() + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) + for i in range(num_answers): + queuekey = 1000 + i + latest_timestamp = datetime.now() + queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp) + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + test_lcp.correct_map.update(cmap) + + # Queue state only tracks up to second + latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp,'%Y%m%d%H%M%S'),'%Y%m%d%H%M%S') + + self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp) def test_convert_files_to_filenames(self): + ''' + Test whether file objects are converted to filenames without altering other structures + ''' problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" fp = open(problem_file) answers_with_file = {'1_2_1': 'String-based answer', diff --git a/common/lib/xmodule/xmodule/tests/test_files/coderesponse.xml b/common/lib/xmodule/xmodule/tests/test_files/coderesponse.xml index 42b6e0a54a..1c0bf8d4e6 100644 --- a/common/lib/xmodule/xmodule/tests/test_files/coderesponse.xml +++ b/common/lib/xmodule/xmodule/tests/test_files/coderesponse.xml @@ -9,91 +9,23 @@ Write a program to compute the square of a number - - + + def square(x): + answer + grader stuff + -Write a program to compute the cube of a number +Write a program to compute the square of a number - - + + def square(x): + answer + grader stuff + diff --git a/common/lib/xmodule/xmodule/tests/test_files/coderesponse_externalresponseformat.xml b/common/lib/xmodule/xmodule/tests/test_files/coderesponse_externalresponseformat.xml new file mode 100644 index 0000000000..42b6e0a54a --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_files/coderesponse_externalresponseformat.xml @@ -0,0 +1,101 @@ + + +

Code response

+ +

+

+ + +Write a program to compute the square of a number + + + + + + + + +Write a program to compute the cube of a number + + + + + + + +
+
diff --git a/lms/envs/test.py b/lms/envs/test.py index 1ab3f550b8..c164889d79 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -58,7 +58,7 @@ XQUEUE_INTERFACE = { }, "basic_auth": ('anant', 'agarwal'), } - +XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds # 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 3787307d93eeb04a37003490c7aa260782fb0477 Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 24 Aug 2012 11:23:26 -0400 Subject: [PATCH 09/90] Put WAITTIME_BETWEEN_REQUESTS in common rather than dev env --- lms/envs/common.py | 3 +++ lms/envs/dev.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 938c4036ae..ce08bf9666 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -86,6 +86,9 @@ DEFAULT_GROUPS = [] # If this is true, random scores will be generated for the purpose of debugging the profile graphs GENERATE_PROFILE_SCORES = False +# Used with XQueue +XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds + ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms REPO_ROOT = PROJECT_ROOT.dirname() diff --git a/lms/envs/dev.py b/lms/envs/dev.py index fe12ee3322..a76e6de262 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -63,7 +63,6 @@ XQUEUE_INTERFACE = { }, "basic_auth": ('anant', 'agarwal'), } -XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds # Make the keyedcache startup warnings go away CACHE_TIMEOUT = 0 From 2cf612051c0cbba063527217c6dd9369b335a62a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 24 Aug 2012 13:53:32 -0400 Subject: [PATCH 10/90] turn subdomain listings off by default in dev env --- lms/envs/dev.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index a76e6de262..97803cb47d 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -15,7 +15,7 @@ TEMPLATE_DEBUG = True MITX_FEATURES['DISABLE_START_DATES'] = True MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True -MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True +MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up MITX_FEATURES['SUBDOMAIN_BRANDING'] = True WIKI_ENABLED = True @@ -107,7 +107,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1'] MITX_FEATURES['AUTH_USE_OPENID'] = True MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True -INSTALLED_APPS += ('external_auth',) +INSTALLED_APPS += ('external_auth',) INSTALLED_APPS += ('django_openid_auth',) OPENID_CREATE_USERS = False From 7a67fdc4ed54cf2f766114b02f7b4c3c5284bca8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 24 Aug 2012 13:54:46 -0400 Subject: [PATCH 11/90] Fix url_name loading - cleaned up name loading - clear fallbacks, including hashing content if no name specified - log errors in error tracker for content devs to see --- common/lib/xmodule/xmodule/modulestore/xml.py | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 3eca72987e..98260f5982 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -1,3 +1,4 @@ +import hashlib import json import logging import os @@ -43,14 +44,76 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): xmlstore: the XMLModuleStore to store the loaded modules in """ - self.unnamed_modules = 0 - self.used_slugs = set() + self.unnamed = defaultdict(int) # category -> num of new url_names for that category + self.used_names = defaultdict(set) # category -> set of used url_names self.org, self.course, self.url_name = course_id.split('/') def process_xml(xml): """Takes an xml string, and returns a XModuleDescriptor created from that xml. """ + + def make_name_unique(xml_data): + """ + Make sure that the url_name of xml_data is unique. If a previously loaded + unnamed descriptor stole this element's url_name, create a new one. + + Removes 'slug' attribute if present, and adds or overwrites the 'url_name' attribute. + """ + # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) + + attr = xml_data.attrib + tag = xml_data.tag + id = lambda x: x + # Things to try to get a name, in order (key, cleaning function, remove key after reading?) + lookups = [('url_name', id, False), + ('slug', id, True), + ('name', Location.clean, False), + ('display_name', Location.clean, False)] + + url_name = None + for key, clean, remove in lookups: + if key in attr: + url_name = clean(attr[key]) + if remove: + del attr[key] + break + + def fallback_name(): + """Return the fallback name for this module. This is a function instead of a variable + because we want it to be lazy.""" + return tag + "_" + hashlib.sha1(xml).hexdigest()[:12] + + # Fallback if there was nothing we could use: + if url_name is None or url_name == "": + # use the hash of the content--the first 12 bytes should be plenty. + url_name = fallback_name() + # Don't log a warning--we don't need this in the log. Do + # put it in the error tracker--content folks need to see it. + need_uniq_names = ('problem', 'sequence', 'video', 'course', 'chapter') + + if tag in need_uniq_names: + error_tracker("ERROR: no name of any kind specified for {tag}. Student " + "state won't work right. Problem xml: '{xml}...'".format(tag=tag, xml=xml[:100])) + else: + # TODO (vshnayder): We may want to enable this once course repos are cleaned up. + # (or we may want to give up on the requirement for non-state-relevant issues...) + #error_tracker("WARNING: no name specified for module. xml='{0}...'".format(xml[:100])) + pass + + # Make sure everything is unique + if url_name in self.used_names[tag]: + msg = ("Non-unique url_name in xml. This may break content. url_name={0}. Content={1}" + .format(url_name, xml[:100])) + error_tracker("ERROR: " + msg) + log.warning(msg) + # Just set name to fallback_name--if there are multiple things with the same fallback name, + # they are actually identical, so it's fragile, but not immediately broken. + url_name = fallback_name() + + self.used_names[tag].add(url_name) + xml_data.set('url_name', url_name) + try: # VS[compat] # TODO (cpennington): Remove this once all fall 2012 courses @@ -62,32 +125,11 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): err=str(err), xml=xml)) raise - # VS[compat]. Take this out once course conversion is done - if xml_data.get('slug') is None and xml_data.get('url_name') is None: - if xml_data.get('name'): - slug = Location.clean(xml_data.get('name')) - elif xml_data.get('display_name'): - slug = Location.clean(xml_data.get('display_name')) - else: - self.unnamed_modules += 1 - slug = '{tag}_{count}'.format(tag=xml_data.tag, - count=self.unnamed_modules) - - while slug in self.used_slugs: - self.unnamed_modules += 1 - slug = '{slug}_{count}'.format(slug=slug, - count=self.unnamed_modules) - - self.used_slugs.add(slug) - # log.debug('-> slug=%s' % slug) - xml_data.set('url_name', slug) + make_name_unique(xml_data) descriptor = XModuleDescriptor.load_from_xml( etree.tostring(xml_data), self, self.org, self.course, xmlstore.default_class) - - #log.debug('==> importing descriptor location %s' % - # repr(descriptor.location)) descriptor.metadata['data_dir'] = course_dir xmlstore.modules[course_id][descriptor.location] = descriptor From dc52956c28d7b112cc4711994ad35a23bb939259 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 24 Aug 2012 14:09:57 -0400 Subject: [PATCH 12/90] don't list all users if too many --- .../management/commands/manage_course_groups.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py index 0043b483e0..f3a39db5ca 100644 --- a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py +++ b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py @@ -36,9 +36,14 @@ class Command(BaseCommand): # users in group uall = User.objects.all() - print "----" - print "List of All Users: %s" % [str(x.username) for x in uall] - print "----" + if uall.count()<50: + print "----" + print "List of All Users: %s" % [str(x.username) for x in uall] + print "----" + else: + print "----" + print "There are %d users, which is too many to list" % uall.count() + print "----" while True: From d1760f9a22a075648190003f9647bc64ba2cbfb0 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Fri, 24 Aug 2012 16:14:04 -0400 Subject: [PATCH 13/90] styled markdown modal and link --- lms/static/sass/course/wiki/_wiki.scss | 78 ++++++++++++++--- lms/templates/wiki/base.html | 5 +- lms/templates/wiki/includes/cheatsheet.html | 95 +++++++++++---------- 3 files changed, 117 insertions(+), 61 deletions(-) diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss index ead58bf6a6..912b416ff6 100644 --- a/lms/static/sass/course/wiki/_wiki.scss +++ b/lms/static/sass/course/wiki/_wiki.scss @@ -567,29 +567,79 @@ section.wiki { background: #f00 !important; } + .cheatsheet { + float: right; + position: relative; + top: -26px; + font-size: 12px; + } + #cheatsheetLink { - text-align:right; - display: float; + text-align: right; + display: float; } #cheatsheetModal { - width: 350px; - margin-left: 100px; - margin-top: -100px; - } + width: 950px; + margin-left: -450px; + margin-top: -100px; + + .left-column { + margin-right: 10px; + } + + .left-column, + .right-column { + float: left; + width: 450px; + } + + .close-btn { + display: block; + position: absolute; + top: -8px; + right: -8px; + width: 30px; + height: 30px; + border-radius: 30px; + border: 1px solid #ccc; + @include linear-gradient(top, #eee, #d2d2d2); + font-size: 22px; + line-height: 28px; + color: #333; + text-align: center; + @include box-shadow(0 1px 0 #fff inset, 0 1px 2px rgba(0, 0, 0, .2)); + } + } #cheatsheet-body { - background: #FFF; - text-align: left; - padding: 10px; + background: #fff; + text-align: left; + padding: 20px; + font-size: 14px; + @include clearfix; + + h3 { + font-weight: bold; + } + + ul { + list-style: circle; + line-height: 1.4; + color: #333; + } + } + + #cheatsheet-body section + section { + margin-top: 40px; } #cheatsheet-body pre{ - color: #000; - text-align: left; - background: #EEE; - margin:10px; - padding: 10px; + color: #000; + text-align: left; + background: #eee; + padding: 10px; + font-size: 12px; } /*----------------- diff --git a/lms/templates/wiki/base.html b/lms/templates/wiki/base.html index a346be0e3e..5c7381ac05 100644 --- a/lms/templates/wiki/base.html +++ b/lms/templates/wiki/base.html @@ -42,9 +42,12 @@ diff --git a/lms/templates/wiki/includes/cheatsheet.html b/lms/templates/wiki/includes/cheatsheet.html index d58920b814..429c1ea22e 100644 --- a/lms/templates/wiki/includes/cheatsheet.html +++ b/lms/templates/wiki/includes/cheatsheet.html @@ -1,53 +1,56 @@ - + + + + From b8659e084ce7edb845bc2f52e7c11597e8773d0e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 24 Aug 2012 16:30:16 -0400 Subject: [PATCH 14/90] Set request.user = student when impersonating a student * currently only in the staff view of student progress page [Fix #34379687] --- lms/djangoapps/courseware/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 83957c17d7..50b7a2d645 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -325,14 +325,17 @@ def progress(request, course_id, student_id=None): raise Http404 student = User.objects.get(id=int(student_id)) + # NOTE: To make sure impersonation by instructor works, use + # student instead of request.user in the rest of the function. + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - course_id, request.user, course) - course_module = get_module(request.user, request, course.location, + course_id, student, course) + course_module = get_module(student, request, course.location, student_module_cache, course_id) courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache) - grade_summary = grades.grade(request.user, request, course, student_module_cache) + grade_summary = grades.grade(student, request, course, student_module_cache) context = {'course': course, 'courseware_summary': courseware_summary, From 45a64a9bd4cc33968c0cf5890eb46e5285f4e36f Mon Sep 17 00:00:00 2001 From: kimth Date: Fri, 24 Aug 2012 16:33:05 -0400 Subject: [PATCH 15/90] Hide queue length information from the student --- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/capa/capa/templates/textbox.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index ad54736359..9290503c9d 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -384,7 +384,7 @@ def textbox(element, value, status, render_template, msg=''): if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue status = 'queued' queue_len = msg - msg = 'Submitted to grader. (Queue length: %s)' % queue_len + msg = 'Submitted to grader.' # For CodeMirror mode = element.get('mode','python') diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/textbox.html index 647f4fe4e8..19c43482a8 100644 --- a/common/lib/capa/capa/templates/textbox.html +++ b/common/lib/capa/capa/templates/textbox.html @@ -21,7 +21,7 @@
% endif
- (${state}) +
${msg|n}
From 7a6fa1dd41a5f6b8b10a5b65f76edbaa59457da9 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 24 Aug 2012 16:52:19 -0400 Subject: [PATCH 16/90] Fix access control for impersonation case - only relevant in tests due to start dates - still irritatingly intricate logic... --- lms/djangoapps/courseware/module_render.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index da9828fb12..8a96d2533f 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -143,8 +143,9 @@ def get_module(user, request, location, student_module_cache, course_id, positio exists. Arguments: - - user : current django User - - request : current django HTTPrequest + - user : User for whom we're getting the module + - request : current django HTTPrequest -- used in particular for auth + (This is important e.g. for prof impersonation of students in progress view) - location : A Location-like object identifying the module to load - student_module_cache : a StudentModuleCache - course_id : the course_id in the context of which to load module @@ -170,7 +171,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi descriptor = modulestore().get_instance(course_id, location) # Short circuit--if the user shouldn't have access, bail without doing any work - if not has_access(user, descriptor, 'load'): + # NOTE: Do access check on request.user -- that's who actually needs access (e.g. could be prof + # impersonating a user) + if not has_access(request.user, descriptor, 'load'): return None #TODO Only check the cache if this module can possibly have state From 1dcaf21f8131edb7f0de95b51fac47ac999c8303 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 25 Aug 2012 07:57:17 -0400 Subject: [PATCH 17/90] Send idhash --- common/lib/capa/capa/responsetypes.py | 10 +++++++++- common/lib/capa/capa/xqueue_interface.py | 7 ++----- lms/djangoapps/courseware/module_render.py | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b2d56b48ca..032970e324 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -16,6 +16,7 @@ import numpy import random import re import requests +import time import traceback import hashlib import abc @@ -1124,7 +1125,9 @@ class CodeResponse(LoncapaResponse): qinterface = self.system.xqueue['interface'] # Generate header - queuekey = xqueue_interface.make_hashkey(str(self.system.seed)+self.answer_id) + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + str(time.time()) + + str(self.system.xqueue['student_identifier']) + + self.answer_id) xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) @@ -1137,6 +1140,11 @@ class CodeResponse(LoncapaResponse): contents = self.payload.copy() + # Anonymized student identifier to the external grader + student_identifier_hash = xqueue_interface.make_hashkey(self.system.xqueue['student_identifier']) + student_info = {'student_identifier_hash': student_identifier_hash} + contents.update({'student_info': json.dumps(student_info)}) + # Submit request. When successful, 'msg' is the prior length of the queue if is_list_of_files(submission): contents.update({'student_response': ''}) # TODO: Is there any information we want to send here? diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 2930eb682d..0dfdabf8d0 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -5,20 +5,17 @@ import hashlib import json import logging import requests -import time log = logging.getLogger('mitx.' + __name__) -def make_hashkey(seed=None): +def make_hashkey(seed): ''' Generate a string key by hashing ''' h = hashlib.md5() - if seed is not None: - h.update(str(seed)) - h.update(str(time.time())) + h.update(str(seed)) return h.hexdigest() diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index da9828fb12..467b147d0f 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -217,7 +217,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi xqueue = {'interface': xqueue_interface, 'callback_url': xqueue_callback_url, - 'default_queuename': xqueue_default_queuename.replace(' ', '_')} + 'default_queuename': xqueue_default_queuename.replace(' ', '_'), + 'student_identifier': user.id, + } def inner_get_module(location): """ From 27459efcf7d2fcc25c63a42a523a38aa1de397e5 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 25 Aug 2012 08:06:17 -0400 Subject: [PATCH 18/90] Update CodeResponse test --- common/lib/xmodule/xmodule/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 2a380bb8be..01462d16ac 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -35,7 +35,7 @@ i4xs = ModuleSystem( user=Mock(), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), debug=True, - xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, + xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'student_identifier': 0}, node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") ) From f7c87aa7a1a318dae587276a489d95e0b2f91b11 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 25 Aug 2012 09:03:47 -0400 Subject: [PATCH 19/90] Anonymous student id is a property of ModuleSystem --- common/lib/capa/capa/responsetypes.py | 8 ++++---- common/lib/xmodule/xmodule/tests/__init__.py | 5 +++-- common/lib/xmodule/xmodule/x_module.py | 11 +++++++++-- lms/djangoapps/courseware/module_render.py | 10 ++++++++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 032970e324..fb7a69a2f2 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1123,10 +1123,11 @@ class CodeResponse(LoncapaResponse): # Prepare xqueue request #------------------------------------------------------------ qinterface = self.system.xqueue['interface'] + anonymous_student_id = self.system.anonymous_student_id # Generate header queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + str(time.time()) + - str(self.system.xqueue['student_identifier']) + + anonymous_student_id + self.answer_id) xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, @@ -1134,15 +1135,14 @@ class CodeResponse(LoncapaResponse): # Generate body if is_list_of_files(submission): - self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here + self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue else: self.context.update({'submission': submission}) contents = self.payload.copy() # Anonymized student identifier to the external grader - student_identifier_hash = xqueue_interface.make_hashkey(self.system.xqueue['student_identifier']) - student_info = {'student_identifier_hash': student_identifier_hash} + student_info = {'anonymous_student_id': anonymous_student_id} contents.update({'student_info': json.dumps(student_info)}) # Submit request. When successful, 'msg' is the prior length of the queue diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 01462d16ac..0d10184501 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -35,8 +35,9 @@ i4xs = ModuleSystem( user=Mock(), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), debug=True, - xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'student_identifier': 0}, - node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") + xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, + node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), + anonymous_student_id = 'student' ) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index c581911c03..0dc16bd976 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -717,7 +717,8 @@ class ModuleSystem(object): filestore=None, debug=False, xqueue=None, - node_path=""): + node_path="", + anonymous_student_id=''): ''' Create a closure around the system environment. @@ -742,11 +743,16 @@ class ModuleSystem(object): at settings.DATA_DIR. xqueue - Dict containing XqueueInterface object, as well as parameters - for the specific StudentModule + for the specific StudentModule: + xqueue = {'interface': XQueueInterface object, + 'callback_url': Callback into the LMS, + 'queue_name': Target queuename in Xqueue} replace_urls - TEMPORARY - A function like static_replace.replace_urls that capa_module can use to fix up the static urls in ajax results. + + anonymous_student_id - Used for tracking modules with student id ''' self.ajax_url = ajax_url self.xqueue = xqueue @@ -758,6 +764,7 @@ class ModuleSystem(object): self.seed = user.id if user is not None else 0 self.replace_urls = replace_urls self.node_path = node_path + self.anonymous_student_id = anonymous_student_id def get(self, attr): ''' provide uniform access to attributes (like etree).''' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 467b147d0f..b278d2615b 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,3 +1,4 @@ +import hashlib import json import logging import sys @@ -173,6 +174,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi if not has_access(user, descriptor, 'load'): return None + # Anonymized student identifier + h = hashlib.md5() # TODO: Seed with LMS secret key + h.update(str(user.id)) + anonymous_student_id = h.hexdigest() + #TODO Only check the cache if this module can possibly have state instance_module = None shared_module = None @@ -218,7 +224,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi xqueue = {'interface': xqueue_interface, 'callback_url': xqueue_callback_url, 'default_queuename': xqueue_default_queuename.replace(' ', '_'), - 'student_identifier': user.id, } def inner_get_module(location): @@ -243,7 +248,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi # a module is coming through get_html and is therefore covered # by the replace_static_urls code below replace_urls=replace_urls, - node_path=settings.NODE_PATH + node_path=settings.NODE_PATH, + anonymous_student_id=anonymous_student_id ) # pass position specified in URL to module through ModuleSystem system.set('position', position) From 25ea8b29905fde7d1a43e85bb2a0264d4a3ac158 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 25 Aug 2012 09:18:10 -0400 Subject: [PATCH 20/90] Use LMS secret key to seed anonymizer --- lms/djangoapps/courseware/module_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index b278d2615b..5bf1cc8e97 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -175,7 +175,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi return None # Anonymized student identifier - h = hashlib.md5() # TODO: Seed with LMS secret key + h = hashlib.md5() + h.update(settings.SECRET_KEY) h.update(str(user.id)) anonymous_student_id = h.hexdigest() From 1f7a1065ef41a1f0efe7fc006f4262002494031e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andre=CC=81s=20Rocha?= Date: Sat, 25 Aug 2012 10:53:20 -0400 Subject: [PATCH 21/90] Important fix! Remove HTML tags from Markdown code. --- repo-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo-requirements.txt b/repo-requirements.txt index 7119106d8b..da38cee406 100644 --- a/repo-requirements.txt +++ b/repo-requirements.txt @@ -1,6 +1,6 @@ -e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles -e git://github.com/MITx/django-pipeline.git#egg=django-pipeline --e git://github.com/benjaoming/django-wiki.git@cd1c23e1#egg=django-wiki +-e git://github.com/rocha/django-wiki.git@33e9a24b9a20#egg=django-wiki -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e common/lib/capa -e common/lib/xmodule From e7b67d7225d526e2c5f6ef671326c7b9adf38ef8 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sat, 25 Aug 2012 12:15:03 -0400 Subject: [PATCH 22/90] Switch django-wiki dependency to a clone in our org so that local changes are easier to make --- repo-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo-requirements.txt b/repo-requirements.txt index da38cee406..f98d05ffc9 100644 --- a/repo-requirements.txt +++ b/repo-requirements.txt @@ -1,6 +1,6 @@ -e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles -e git://github.com/MITx/django-pipeline.git#egg=django-pipeline --e git://github.com/rocha/django-wiki.git@33e9a24b9a20#egg=django-wiki +-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e common/lib/capa -e common/lib/xmodule From eb8ee01d21fe50a50454242bc35477eebf0a5153 Mon Sep 17 00:00:00 2001 From: Rocky Duan Date: Sat, 25 Aug 2012 12:12:57 -0700 Subject: [PATCH 23/90] fixed a tiny bug that loading icon sometimes doesn't appear at the right place / doesn't go away --- lms/static/coffee/src/discussion/content.coffee | 2 +- lms/static/coffee/src/discussion/utils.coffee | 5 +++-- lms/templates/discussion/mustache/_content.mustache | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index 13d6f1094f..73c3688c2a 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -124,7 +124,7 @@ if Backbone? url = @model.urlFor('retrieve') DiscussionUtil.safeAjax $elem: $elem - $loading: $(event.target) if event + $loading: @$(".discussion-show-comments") type: "GET" url: url success: (response, textStatus) => diff --git a/lms/static/coffee/src/discussion/utils.coffee b/lms/static/coffee/src/discussion/utils.coffee index 94807654c9..0990f0c87c 100644 --- a/lms/static/coffee/src/discussion/utils.coffee +++ b/lms/static/coffee/src/discussion/utils.coffee @@ -1,9 +1,10 @@ $ -> $.fn.extend loading: -> - $(this).after("") + @$_loading = $("") + $(this).after(@$_loading) loaded: -> - $(this).parent().children(".discussion-loading").remove() + @$_loading.remove() class @DiscussionUtil diff --git a/lms/templates/discussion/mustache/_content.mustache b/lms/templates/discussion/mustache/_content.mustache index a5089793e7..b4f3176931 100644 --- a/lms/templates/discussion/mustache/_content.mustache +++ b/lms/templates/discussion/mustache/_content.mustache @@ -7,11 +7,11 @@
{{#thread}} From c406be8c6a28db205bb2417dab1dd912f5475385 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 26 Aug 2012 13:43:21 +0000 Subject: [PATCH 24/90] Check grader message has proper XML structure --- common/lib/capa/capa/responsetypes.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b2d56b48ca..92c6f62048 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1165,7 +1165,7 @@ class CodeResponse(LoncapaResponse): (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: - oldcmap.set(self.answer_id, msg='Error: Invalid grader reply.') + oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.') return oldcmap correctness = 'correct' if correct else 'incorrect' @@ -1203,10 +1203,10 @@ class CodeResponse(LoncapaResponse): Returns (valid_score_msg, correct, score, msg): valid_score_msg: Flag indicating valid score_msg format (Boolean) correct: Correctness of submission (Boolean) - score: # TODO: Implement partial grading + score: Points to be assigned (numeric, can be float) msg: Message from grader to display to student (string) ''' - fail = (False, False, -1, '') + fail = (False, False, 0, '') try: score_result = json.loads(score_msg) except (TypeError, ValueError): @@ -1216,7 +1216,19 @@ class CodeResponse(LoncapaResponse): for tag in ['correct', 'score', 'msg']: if not score_result.has_key(tag): return fail - return (True, score_result['correct'], score_result['score'], score_result['msg']) + + # Next, we need to check that the contents of the external grader message + # is safe for the LMS. + # 1) Make sure that the message is valid XML (proper opening/closing tags) + # 2) TODO: Is the message actually HTML? + msg = score_result['msg'] + try: + etree.fromstring(msg) + except etree.XMLSyntaxError as err: + log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg) + return fail + + return (True, score_result['correct'], score_result['score'], msg) #----------------------------------------------------------------------------- From 79568dfdf68ced51ec739ddcb7da2ea6a89e10d6 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 26 Aug 2012 17:37:01 +0000 Subject: [PATCH 25/90] Add error logging for external grader messages --- common/lib/capa/capa/responsetypes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 92c6f62048..e7d7406cbd 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1210,11 +1210,14 @@ class CodeResponse(LoncapaResponse): try: score_result = json.loads(score_msg) except (TypeError, ValueError): + log.error("External grader message should be a JSON-serialized dict") return fail if not isinstance(score_result, dict): + log.error("External grader message should be a JSON-serialized dict") return fail for tag in ['correct', 'score', 'msg']: if not score_result.has_key(tag): + log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'") return fail # Next, we need to check that the contents of the external grader message From af871d92aa9165668ad920c8b54a8549fb56f652 Mon Sep 17 00:00:00 2001 From: kimth Date: Sun, 26 Aug 2012 21:15:34 +0000 Subject: [PATCH 26/90] Send ext grader the time of submission --- common/lib/capa/capa/responsetypes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index fb7a69a2f2..580c4f979a 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -27,6 +27,7 @@ import xml.sax.saxutils as saxutils # specific library imports from calc import evaluator, UndefinedVariable from correctmap import CorrectMap +from datetime import datetime from util import * from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? @@ -1141,8 +1142,11 @@ class CodeResponse(LoncapaResponse): contents = self.payload.copy() - # Anonymized student identifier to the external grader - student_info = {'anonymous_student_id': anonymous_student_id} + # Metadata related to the student submission revealed to the external grader + current_time = datetime.now() + student_info = {'anonymous_student_id': anonymous_student_id, + 'submission_time': str(current_time), + } contents.update({'student_info': json.dumps(student_info)}) # Submit request. When successful, 'msg' is the prior length of the queue From 25c83bbb5d447cee8b3617193f014106a17699dc Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sun, 26 Aug 2012 21:55:55 -0400 Subject: [PATCH 27/90] xml format docs! --- doc/xml-format.md | 370 +++++++++++++++++++++++++++++++++------------- 1 file changed, 264 insertions(+), 106 deletions(-) diff --git a/doc/xml-format.md b/doc/xml-format.md index 2a9e379ccc..55bcda4480 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -1,147 +1,305 @@ -This doc is a rough spec of our xml format +# edX xml format tutorial -Every content element (within a course) should have a unique id. This id is formed as {category}/{url_name}. Categories are the different tag types ('chapter', 'problem', 'html', 'sequential', etc). Url_name is a string containing a-z, A-Z, dot (.) and _. This is what appears in urls that point to this object. +## Goals of this document -File layout: +* This was written assuming the reader has no prior programming/CS knowledge and has jumped cold turkey into the edX platform. +* To educate the reader on how to build and maintain the back end structure of the course content. This is important for debugging and standardization. +* After reading this, you should be able to add content to a course and make sure it shows up in the courseware and does not break the code. +* __Prerequisites:__ it would be helpful to know a little bit about xml. Here is a [simple example](http://www.ultraslavonic.info/intro-to-xml/) if you've never seen it before. -- Xml files have content -- "policy", which is also called metadata in various places, should live in a policy file. +## Outline -- each module (except customtag and course, which are special, see below) should live in a file, located at {category}/{url_name].xml -To include this module in another one (e.g. to put a problem in a vertical), put in a "pointer tag": <{category} url_name="{url_name}"/>. When we read that, we'll load the actual contents. +* First, we will show a sample course structure as a case study/model of how xml and files in a course are organized to introductory understanding. -Customtag is already a pointer, you can just use it in place: +* More technical details are below, including discussion of some special cases. -Course tags: - - the top level course pointer tag lives in course.xml - - have 2 extra required attributes: "org" and "course" -- organization name, and course name. Note that the course name is referring to the platonic ideal of this course, not to any particular run of this course. The url_name should be particular run of this course. E.g. -If course.xml contains: - +## Introduction -we would load the actual course definition from course/2012.xml +* The course is organized hierarchically. We start by describing course-wide parameters, then break the course into chapters, and then go deeper and deeper until we reach a specific pset, video, etc. -To support multiple different runs of the course, you could have a different course.xml, containing +* You could make an analogy to finding a green shirt in your house - front door -> bedroom -> closet -> drawer -> shirts -> green shirt - -which would load the Harvard-internal version from course/2012H.xml +## Case Study -If there is only one run of the course for now, just have a single course.xml with the right url_name. +Let's jump right in by looking at the directory structure of a very simple toy course: -If there is more than one run of the course, the different course root pointer files should live in -roots/url_name.xml, and course.xml should be a symbolic link to the one you want to run in your dev instance. + toy/ + course + course.xml + problem + policies + roots -If you want to run both versions, you need to checkout the repo twice, and have course.xml point to different root/{url_name}.xml files. +The only top level file is `course.xml`, which should contain one line, looking something like this: -Policies: - - the policy for a course url_name lives in policies/{url_name}.json + -The format is called "json", and is best shown by example (though also feel free to google :) +This gives all the information to uniquely identify a particular run of any course--which organization is producing the course, what the course name is, and what "run" this is, specified via the `url_name` attribute. -the file is a dictionary (mapping from keys to values, syntax "{ key : value, key2 : value2, etc}" +Obviously, this doesn't actually specify any of the course content, so we need to find that next. To know where to look, you need to know the standard organizational structure of our system: _course elements are uniquely identified by the combination `(category, url_name)`_. In this case, we are looking for a `course` element with the `url_name` "2012_Fall". The definition of this element will be in `course/2012_Fall.xml`. Let's look there next: -Keys are in the form "{category}/{url_name}", which should uniquely id a content element. -Values are dictionaries of the form {"metadata-key" : "metadata-value"}. +`course/2012_Fall.xml` -metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, please be consistent (e.g. if display_names stay in xml, they should all stay in xml). - - note, some xml attributes are not metadata. e.g. in
- {% include "wiki/includes/cheatsheet.html" %} - {% endblock %} diff --git a/lms/templates/wiki/create.html b/lms/templates/wiki/create.html index 886764ba84..745be08cf8 100644 --- a/lms/templates/wiki/create.html +++ b/lms/templates/wiki/create.html @@ -42,6 +42,7 @@ {% trans "Go back" %} + {% include "wiki/includes/cheatsheet.html" %} diff --git a/lms/templates/wiki/edit.html b/lms/templates/wiki/edit.html index f4bd7d138f..65378da4e4 100644 --- a/lms/templates/wiki/edit.html +++ b/lms/templates/wiki/edit.html @@ -40,7 +40,10 @@ + {% include "wiki/includes/cheatsheet.html" %} {% endblock %} + + diff --git a/lms/templates/wiki/includes/cheatsheet.html b/lms/templates/wiki/includes/cheatsheet.html index 429c1ea22e..6f5170e601 100644 --- a/lms/templates/wiki/includes/cheatsheet.html +++ b/lms/templates/wiki/includes/cheatsheet.html @@ -1,4 +1,4 @@ - + diff --git a/lms/templates/wiki/includes/editor_widget.html b/lms/templates/wiki/includes/editor_widget.html new file mode 100644 index 0000000000..2f5dd7ce68 --- /dev/null +++ b/lms/templates/wiki/includes/editor_widget.html @@ -0,0 +1,4 @@ + +

+ Markdown syntax is allowed. See the cheatsheet for help. +

From 57c6bfc3a6239e91caaf2237b98deb862127e27e Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Mon, 27 Aug 2012 18:46:13 -0400 Subject: [PATCH 42/90] commenting out the pre-loading of modules until we get a better way in place (that doesn't hang on deploy) --- common/lib/xmodule/xmodule/modulestore/django.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index d46422cbf1..0b86c2fea4 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -44,7 +44,7 @@ def modulestore(name='default'): return _MODULESTORES[name] -if 'DJANGO_SETTINGS_MODULE' in environ: - # Initialize the modulestores immediately - for store_name in settings.MODULESTORE: - modulestore(store_name) +# if 'DJANGO_SETTINGS_MODULE' in environ: +# # Initialize the modulestores immediately +# for store_name in settings.MODULESTORE: +# modulestore(store_name) From c462b1a917292f400db7a2cf126622e22a04fce2 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 18:50:31 -0400 Subject: [PATCH 43/90] Document CorrectMap.set --- common/lib/capa/capa/correctmap.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 71f97e1321..112976389c 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -32,6 +32,7 @@ class CorrectMap(object): def __iter__(self): return self.cmap.__iter__() + # See the documentation for 'set_dict' for the use of kwargs def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs): if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, @@ -53,9 +54,19 @@ class CorrectMap(object): def set_dict(self, correct_map): ''' - set internal dict to provided correct_map dict - for graceful migration, if correct_map is a one-level dict, then convert it to the new - dict of dicts format. + Set internal dict of CorrectMap to provided correct_map dict + + correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that + when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict + not coincide with the newest CorrectMap format as defined by self.set. + + For graceful migration, feed the contents of each correct map to self.set, rather than + making a direct copy of the given correct_map dict. This way, the common keys between + the incoming correct_map dict and the new CorrectMap instance will be written, while + mismatched keys will be gracefully ignored. + + Special migration case: + If correct_map is a one-level dict, then convert it to the new dict of dicts format. ''' if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): self.__init__() # empty current dict From 0cff6fa30dd3ece7af80a3b73e4f200274f84532 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 18:51:38 -0400 Subject: [PATCH 44/90] Remove debugging comment --- common/lib/capa/capa/capa_problem.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 8edc65b9f9..d7bae1a36c 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -98,10 +98,7 @@ class LoncapaProblem(object): if 'student_answers' in state: self.student_answers = state['student_answers'] if 'correct_map' in state: - print 'THK: LoncapaProblem.__init__' - print json.dumps(state['correct_map'], indent=4) self.correct_map.set_dict(state['correct_map']) - print json.dumps(self.correct_map.get_dict(), indent=4) if 'done' in state: self.done = state['done'] From dca16ad007b60f093c36cbf126e986e1e85730bb Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:01:36 -0400 Subject: [PATCH 45/90] CorrectMap uses dict to represent queuestate --- common/lib/capa/capa/correctmap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 112976389c..07454471f0 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -15,7 +15,7 @@ class CorrectMap(object): - msg : string (may have HTML) giving extra message response (displayed below textline or textbox) - hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) - hintmode : one of (None,'on_request','always') criteria for displaying hint - - queuestate : Tuple (key, time) where key is a secret string, and time is a string dump + - queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued Behaves as a dict. @@ -83,10 +83,10 @@ class CorrectMap(object): return answer_id in self.cmap and self.cmap[answer_id]['queuestate'] is not None def is_right_queuekey(self, answer_id, test_key): - return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate'][0] == test_key + return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate']['key'] == test_key def get_queuetime_str(self, answer_id): - return self.cmap[answer_id]['queuestate'][1] if self.is_queued(answer_id) else None + return self.cmap[answer_id]['queuestate']['time'] if self.is_queued(answer_id) else None def get_npoints(self, answer_id): npoints = self.get_property(answer_id, 'npoints') From 50481c2a81f6659ee162a0e12b8bd69e998a52e1 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:04:36 -0400 Subject: [PATCH 46/90] CodeResponse saves queuestate as dict --- common/lib/capa/capa/responsetypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 5cf45adce2..c997cec30b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1151,7 +1151,9 @@ class CodeResponse(LoncapaResponse): # State associated with the queueing request qtime = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S') - queuestate = (queuekey, qtime) + queuestate = {'key': queuekey, + 'time': qtime, + } cmap = CorrectMap() if error: From a9a73561f434972515984eff34d7d30b7c3afddf Mon Sep 17 00:00:00 2001 From: John Hess Date: Mon, 27 Aug 2012 19:05:00 -0400 Subject: [PATCH 47/90] updated admin_dashboard to show count of unique students --- lms/djangoapps/dashboard/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/dashboard/views.py b/lms/djangoapps/dashboard/views.py index c4446bceaa..a1a3461953 100644 --- a/lms/djangoapps/dashboard/views.py +++ b/lms/djangoapps/dashboard/views.py @@ -21,11 +21,16 @@ def dashboard(request): if not request.user.is_staff: raise Http404 - query = "select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc" + queries=[] + queries.append("select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc;") + queries.append("select count(distinct user_id) as unique_students from student_courseenrollment;") from django.db import connection cursor = connection.cursor() - cursor.execute(query) - results = dictfetchall(cursor) + results =[] + + for query in queries: + cursor.execute(query) + results.append(dictfetchall(cursor)) return HttpResponse(json.dumps(results, indent=4)) From 98542f79ece0d1755a1a04b9041f995784227dff Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:14:47 -0400 Subject: [PATCH 48/90] Define dateformat variable in xqueue_interface --- common/lib/capa/capa/capa_problem.py | 7 +++++-- common/lib/capa/capa/responsetypes.py | 2 +- common/lib/capa/capa/xqueue_interface.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index d7bae1a36c..9dc7658884 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -34,6 +34,7 @@ from datetime import datetime import eia import inputtypes from util import contextualize_text, convert_files_to_filenames +import xqueue_interface # to be replaced with auto-registering import responsetypes @@ -215,8 +216,10 @@ class LoncapaProblem(object): return None # Get a list of timestamps of all queueing requests, then convert it to a DateTime object - queuetimes = [self.correct_map.get_queuetime_str(answer_id) for answer_id in self.correct_map if self.correct_map.is_queued(answer_id)] - queuetimes = [datetime.strptime(qt,'%Y%m%d%H%M%S') for qt in queuetimes] + queuetime_strs = [self.correct_map.get_queuetime_str(answer_id) + for answer_id in self.correct_map + if self.correct_map.is_queued(answer_id)] + queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) for qt_str in queuetime_strs] return max(queuetimes) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index c997cec30b..2ab5f3eafd 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1150,7 +1150,7 @@ class CodeResponse(LoncapaResponse): body=json.dumps(contents)) # State associated with the queueing request - qtime = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S') + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) queuestate = {'key': queuekey, 'time': qtime, } diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 2930eb682d..519f63cacd 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -9,7 +9,7 @@ import time log = logging.getLogger('mitx.' + __name__) - +dateformat = '%Y%m%d%H%M%S' def make_hashkey(seed=None): ''' From 4468fee8e5948d63506b2bfe6e6adb5239cc8000 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:15:28 -0400 Subject: [PATCH 49/90] Define dateformat variable in xqueue_interface --- common/lib/capa/capa/capa_problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 9dc7658884..3a847f8306 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -205,7 +205,7 @@ class LoncapaProblem(object): ''' Returns True if any part of the problem has been submitted to an external queue ''' - return any([self.correct_map.is_queued(answer_id) for answer_id in self.correct_map]) + return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map) def get_recentmost_queuetime(self): From 03588c094c355422821238cf81e3aa0a49934c6e Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:17:00 -0400 Subject: [PATCH 50/90] Move stdlib import location --- common/lib/capa/capa/capa_problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 3a847f8306..f386c9fe24 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -14,6 +14,7 @@ This is used by capa_module. from __future__ import division +from datetime import datetime import json import logging import math @@ -30,7 +31,6 @@ from xml.sax.saxutils import unescape import calc from correctmap import CorrectMap -from datetime import datetime import eia import inputtypes from util import contextualize_text, convert_files_to_filenames From 572e88d5b20fb5a7d326623615922885c328c3af Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:20:50 -0400 Subject: [PATCH 51/90] Adjust wait message --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 33a2f6b4c4..0f97de6f3f 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -469,7 +469,7 @@ class CapaModule(XModule): prev_submit_time = self.lcp.get_recentmost_queuetime() waittime_between_requests = settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: - msg = 'You must wait at least %d seconds between queue submissions' % waittime_between_requests + msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests return {'success': msg, 'html': ''} try: From 2cf4b28cf721b96704d0c82387ff371c56fce4c9 Mon Sep 17 00:00:00 2001 From: John Hess Date: Mon, 27 Aug 2012 19:22:19 -0400 Subject: [PATCH 52/90] added additional query to show number of users with any given number of registrations (power users) --- lms/djangoapps/dashboard/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/dashboard/views.py b/lms/djangoapps/dashboard/views.py index a1a3461953..964b3fac4a 100644 --- a/lms/djangoapps/dashboard/views.py +++ b/lms/djangoapps/dashboard/views.py @@ -24,7 +24,8 @@ def dashboard(request): queries=[] queries.append("select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc;") queries.append("select count(distinct user_id) as unique_students from student_courseenrollment;") - + queries.append("select registrations, count(registrations) from (select count(user_id) as registrations from student_courseenrollment group by user_id) as registrations_per_user group by registrations;") + from django.db import connection cursor = connection.cursor() results =[] From a197f634ea8047da38cd7b388de230ce948a96a4 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:34:46 -0400 Subject: [PATCH 53/90] Get_queuetime_str does not return None --- common/lib/capa/capa/correctmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 07454471f0..52411a8e8c 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -86,7 +86,7 @@ class CorrectMap(object): return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate']['key'] == test_key def get_queuetime_str(self, answer_id): - return self.cmap[answer_id]['queuestate']['time'] if self.is_queued(answer_id) else None + return self.cmap[answer_id]['queuestate']['time'] def get_npoints(self, answer_id): npoints = self.get_property(answer_id, 'npoints') From 26051f9939c4f4597d56146c70ed743f0ede3e72 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:37:46 -0400 Subject: [PATCH 54/90] Waittime passed through ModuleSystem.xqueue --- common/lib/xmodule/xmodule/capa_module.py | 3 +-- lms/djangoapps/courseware/module_render.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 0f97de6f3f..ef3de02b37 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -8,7 +8,6 @@ import re import sys from datetime import timedelta -from django.conf import settings from lxml import etree from pkg_resources import resource_string @@ -467,7 +466,7 @@ class CapaModule(XModule): if self.lcp.is_queued(): current_time = datetime.datetime.now() prev_submit_time = self.lcp.get_recentmost_queuetime() - waittime_between_requests = settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS + waittime_between_requests = self.system.xqueue.waittime if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests return {'success': msg, 'html': ''} diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index da9828fb12..0eaa1ee60f 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -217,7 +217,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi xqueue = {'interface': xqueue_interface, 'callback_url': xqueue_callback_url, - 'default_queuename': xqueue_default_queuename.replace(' ', '_')} + 'default_queuename': xqueue_default_queuename.replace(' ', '_'), + 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS + } def inner_get_module(location): """ From 3051c6aca719be45d5cf7bc977ef4624fa3a8f3d Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:41:35 -0400 Subject: [PATCH 55/90] Access system.xqueue properly as dict --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index ef3de02b37..27641af3b5 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -466,7 +466,7 @@ class CapaModule(XModule): if self.lcp.is_queued(): current_time = datetime.datetime.now() prev_submit_time = self.lcp.get_recentmost_queuetime() - waittime_between_requests = self.system.xqueue.waittime + waittime_between_requests = self.system.xqueue['waittime'] if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests return {'success': msg, 'html': ''} From 093f08064114acf398118240bc7819c15c32a13a Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:45:39 -0400 Subject: [PATCH 56/90] Filesubmission input does not reveal queue length --- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/capa/capa/templates/filesubmission.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 9290503c9d..a323a5dbfe 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -351,7 +351,7 @@ def filesubmission(element, value, status, render_template, msg=''): if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue status = 'queued' queue_len = msg - msg = 'Submitted to grader. (Queue length: %s)' % queue_len + msg = 'Submitted to grader.' context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, 'queue_len': queue_len, 'allowed_files': allowed_files, diff --git a/common/lib/capa/capa/templates/filesubmission.html b/common/lib/capa/capa/templates/filesubmission.html index a859dc8458..e9fd7c5674 100644 --- a/common/lib/capa/capa/templates/filesubmission.html +++ b/common/lib/capa/capa/templates/filesubmission.html @@ -10,7 +10,7 @@ % endif - (${state}) +
${msg|n}
From 7c5879a1a58fcf291f63be0acc8943fd76db569e Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:50:47 -0400 Subject: [PATCH 57/90] Use os.path.join --- common/lib/xmodule/xmodule/tests/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index bd5d35c75f..474ee1280a 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -19,6 +19,7 @@ import capa.calc as calc import capa.capa_problem as lcp from capa.correctmap import CorrectMap from capa.util import convert_files_to_filenames +from capa.xqueue_interface import dateformat from datetime import datetime from xmodule import graders, x_module from xmodule.x_module import ModuleSystem @@ -36,7 +37,7 @@ i4xs = ModuleSystem( user=Mock(), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), debug=True, - xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, + xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10}, node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") ) @@ -286,14 +287,14 @@ class CodeResponseTest(unittest.TestCase): ''' @staticmethod def make_queuestate(key, time): - timestr = datetime.strftime(time,'%Y%m%d%H%M%S') - return (key, timestr) + timestr = datetime.strftime(time, dateformat) + return {'key': key, 'time': timestr} def test_is_queued(self): ''' Simple test of whether LoncapaProblem knows when it's been queued ''' - problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers().keys()) From 2919389d8e5cabfb066960818231e0a2360949a4 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:53:10 -0400 Subject: [PATCH 58/90] Use os.path.join -- all of them... --- common/lib/xmodule/xmodule/tests/__init__.py | 41 ++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 474ee1280a..5b1843a632 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -295,33 +295,34 @@ class CodeResponseTest(unittest.TestCase): Simple test of whether LoncapaProblem knows when it's been queued ''' problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) - - answer_ids = sorted(test_lcp.get_question_answers().keys()) - num_answers = len(answer_ids) + with open(problem_file) as input_file: + test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) + + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) - # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state - cmap = CorrectMap() - for i in range(num_answers): - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) - test_lcp.correct_map.update(cmap) + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for i in range(num_answers): + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + test_lcp.correct_map.update(cmap) - self.assertEquals(test_lcp.is_queued(), False) + self.assertEquals(test_lcp.is_queued(), False) - # Now we queue the LCP - cmap = CorrectMap() - for i in range(num_answers): - queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - test_lcp.correct_map.update(cmap) + # Now we queue the LCP + cmap = CorrectMap() + for i in range(num_answers): + queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + test_lcp.correct_map.update(cmap) - self.assertEquals(test_lcp.is_queued(), True) + self.assertEquals(test_lcp.is_queued(), True) def test_update_score(self): ''' Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem ''' - problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers().keys()) @@ -377,7 +378,7 @@ class CodeResponseTest(unittest.TestCase): ''' Test whether the LoncapaProblem knows about the time of queue requests ''' - problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers().keys()) @@ -411,7 +412,7 @@ class CodeResponseTest(unittest.TestCase): ''' Test whether file objects are converted to filenames without altering other structures ''' - problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" + problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") fp = open(problem_file) answers_with_file = {'1_2_1': 'String-based answer', '1_3_1': ['answer1', 'answer2', 'answer3'], From b4b8f6bc7d69d2771bf287cd2fc987807f821630 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:54:58 -0400 Subject: [PATCH 59/90] Use with open(file) structure --- common/lib/xmodule/xmodule/tests/__init__.py | 156 ++++++++++--------- 1 file changed, 79 insertions(+), 77 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 5b1843a632..5385b489b2 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -323,55 +323,56 @@ class CodeResponseTest(unittest.TestCase): Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem ''' problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) + with open(problem_file) as input_file: + test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) - answer_ids = sorted(test_lcp.get_question_answers().keys()) - num_answers = len(answer_ids) - - # CodeResponse requires internal CorrectMap state. Build it now in the queued state - old_cmap = CorrectMap() - for i in range(num_answers): - queuekey = 1000 + i - queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now()) - old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - - # Message format common to external graders - correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'}) - incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'}) - - xserver_msgs = {'correct': correct_score_msg, - 'incorrect': incorrect_score_msg,} - - # Incorrect queuekey, state should not be updated - for correctness in ['correct', 'incorrect']: - test_lcp.correct_map = CorrectMap() - test_lcp.correct_map.update(old_cmap) # Deep copy - - test_lcp.update_score(xserver_msgs[correctness], queuekey=0) - self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + old_cmap = CorrectMap() for i in range(num_answers): - self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered + queuekey = 1000 + i + queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now()) + old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - # Correct queuekey, state should be updated - for correctness in ['correct', 'incorrect']: - for i in range(num_answers): # Target specific answer_id's + # Message format common to external graders + correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'}) + incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'}) + + xserver_msgs = {'correct': correct_score_msg, + 'incorrect': incorrect_score_msg,} + + # Incorrect queuekey, state should not be updated + for correctness in ['correct', 'incorrect']: test_lcp.correct_map = CorrectMap() - test_lcp.correct_map.update(old_cmap) + test_lcp.correct_map.update(old_cmap) # Deep copy - new_cmap = CorrectMap() - new_cmap.update(old_cmap) - npoints = 1 if correctness=='correct' else 0 - new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) + test_lcp.update_score(xserver_msgs[correctness], queuekey=0) + self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison - test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) - self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) + for i in range(num_answers): + self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered - for j in range(num_answers): - if j == i: - self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered - else: - self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered + # Correct queuekey, state should be updated + for correctness in ['correct', 'incorrect']: + for i in range(num_answers): # Target specific answer_id's + test_lcp.correct_map = CorrectMap() + test_lcp.correct_map.update(old_cmap) + + new_cmap = CorrectMap() + new_cmap.update(old_cmap) + npoints = 1 if correctness=='correct' else 0 + new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) + + test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) + self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) + + for j in range(num_answers): + if j == i: + self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered + else: + self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered def test_recentmost_queuetime(self): @@ -379,48 +380,49 @@ class CodeResponseTest(unittest.TestCase): Test whether the LoncapaProblem knows about the time of queue requests ''' problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) + with open(problem_file) as input_file: + test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) - answer_ids = sorted(test_lcp.get_question_answers().keys()) - num_answers = len(answer_ids) + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) - # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state - cmap = CorrectMap() - for i in range(num_answers): - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) - test_lcp.correct_map.update(cmap) - - self.assertEquals(test_lcp.get_recentmost_queuetime(), None) + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for i in range(num_answers): + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + test_lcp.correct_map.update(cmap) + + self.assertEquals(test_lcp.get_recentmost_queuetime(), None) - # CodeResponse requires internal CorrectMap state. Build it now in the queued state - cmap = CorrectMap() - answer_ids = sorted(test_lcp.get_question_answers().keys()) - num_answers = len(answer_ids) - for i in range(num_answers): - queuekey = 1000 + i - latest_timestamp = datetime.now() - queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp) - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - test_lcp.correct_map.update(cmap) + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + cmap = CorrectMap() + answer_ids = sorted(test_lcp.get_question_answers().keys()) + num_answers = len(answer_ids) + for i in range(num_answers): + queuekey = 1000 + i + latest_timestamp = datetime.now() + queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp) + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + test_lcp.correct_map.update(cmap) - # Queue state only tracks up to second - latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp,'%Y%m%d%H%M%S'),'%Y%m%d%H%M%S') + # Queue state only tracks up to second + latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp,'%Y%m%d%H%M%S'),'%Y%m%d%H%M%S') - self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp) + self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp) - def test_convert_files_to_filenames(self): - ''' - Test whether file objects are converted to filenames without altering other structures - ''' - problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - fp = open(problem_file) - answers_with_file = {'1_2_1': 'String-based answer', - '1_3_1': ['answer1', 'answer2', 'answer3'], - '1_4_1': [fp, fp]} - answers_converted = convert_files_to_filenames(answers_with_file) - self.assertEquals(answers_converted['1_2_1'], 'String-based answer') - self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) - self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) + def test_convert_files_to_filenames(self): + ''' + Test whether file objects are converted to filenames without altering other structures + ''' + problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") + with open(problem_file) as fp: + answers_with_file = {'1_2_1': 'String-based answer', + '1_3_1': ['answer1', 'answer2', 'answer3'], + '1_4_1': [fp, fp]} + answers_converted = convert_files_to_filenames(answers_with_file) + self.assertEquals(answers_converted['1_2_1'], 'String-based answer') + self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) + self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) class ChoiceResponseTest(unittest.TestCase): From 6302283183fe8adff82aaa02a096be15814e47c5 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:57:46 -0400 Subject: [PATCH 60/90] Drop unnecessary .keys() --- common/lib/xmodule/xmodule/tests/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 5385b489b2..4c97710607 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -298,7 +298,7 @@ class CodeResponseTest(unittest.TestCase): with open(problem_file) as input_file: test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) - answer_ids = sorted(test_lcp.get_question_answers().keys()) + answer_ids = sorted(test_lcp.get_question_answers()) num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state @@ -326,7 +326,7 @@ class CodeResponseTest(unittest.TestCase): with open(problem_file) as input_file: test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) - answer_ids = sorted(test_lcp.get_question_answers().keys()) + answer_ids = sorted(test_lcp.get_question_answers()) num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the queued state @@ -383,7 +383,7 @@ class CodeResponseTest(unittest.TestCase): with open(problem_file) as input_file: test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) - answer_ids = sorted(test_lcp.get_question_answers().keys()) + answer_ids = sorted(test_lcp.get_question_answers()) num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state @@ -396,7 +396,6 @@ class CodeResponseTest(unittest.TestCase): # CodeResponse requires internal CorrectMap state. Build it now in the queued state cmap = CorrectMap() - answer_ids = sorted(test_lcp.get_question_answers().keys()) num_answers = len(answer_ids) for i in range(num_answers): queuekey = 1000 + i From 297df37fc05bfef4f674bc4661cacbe09d6969e1 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 19:59:09 -0400 Subject: [PATCH 61/90] Drop unnecessary iterator index --- common/lib/xmodule/xmodule/tests/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 4c97710607..b19b7e012b 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -303,8 +303,8 @@ class CodeResponseTest(unittest.TestCase): # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state cmap = CorrectMap() - for i in range(num_answers): - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + for answer_id in answer_ids: + cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) test_lcp.correct_map.update(cmap) self.assertEquals(test_lcp.is_queued(), False) From 82a0d065a76e644fe92723cae8c1fa9929a1c138 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 20:04:48 -0400 Subject: [PATCH 62/90] Use enumerate in loops --- common/lib/xmodule/xmodule/tests/__init__.py | 32 +++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index b19b7e012b..a4c02a17f4 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -299,7 +299,6 @@ class CodeResponseTest(unittest.TestCase): test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers()) - num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state cmap = CorrectMap() @@ -311,7 +310,7 @@ class CodeResponseTest(unittest.TestCase): # Now we queue the LCP cmap = CorrectMap() - for i in range(num_answers): + for i, answer_id in enumerate(answer_ids): queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) test_lcp.correct_map.update(cmap) @@ -327,11 +326,10 @@ class CodeResponseTest(unittest.TestCase): test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers()) - num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the queued state old_cmap = CorrectMap() - for i in range(num_answers): + for i, answer_id in enumerate(answer_ids): queuekey = 1000 + i queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now()) old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) @@ -351,28 +349,28 @@ class CodeResponseTest(unittest.TestCase): test_lcp.update_score(xserver_msgs[correctness], queuekey=0) self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison - for i in range(num_answers): - self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered + for answer_id in answer_ids: + self.assertTrue(test_lcp.correct_map.is_queued(answer_id)) # Should be still queued, since message undelivered # Correct queuekey, state should be updated for correctness in ['correct', 'incorrect']: - for i in range(num_answers): # Target specific answer_id's + for i, answer_id in enumerate(answer_ids): test_lcp.correct_map = CorrectMap() test_lcp.correct_map.update(old_cmap) new_cmap = CorrectMap() new_cmap.update(old_cmap) npoints = 1 if correctness=='correct' else 0 - new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) + new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) - for j in range(num_answers): + for j, test_id in enumerate(answer_ids): if j == i: - self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered + self.assertFalse(test_lcp.correct_map.is_queued(test_id)) # Should be dequeued, message delivered else: - self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered + self.assertTrue(test_lcp.correct_map.is_queued(test_id)) # Should be queued, message undelivered def test_recentmost_queuetime(self): @@ -384,28 +382,26 @@ class CodeResponseTest(unittest.TestCase): test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs) answer_ids = sorted(test_lcp.get_question_answers()) - num_answers = len(answer_ids) # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state cmap = CorrectMap() - for i in range(num_answers): - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=None)) + for answer_id in answer_ids: + cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) test_lcp.correct_map.update(cmap) self.assertEquals(test_lcp.get_recentmost_queuetime(), None) # CodeResponse requires internal CorrectMap state. Build it now in the queued state cmap = CorrectMap() - num_answers = len(answer_ids) - for i in range(num_answers): + for i, answer_id in enumerate(answer_ids): queuekey = 1000 + i latest_timestamp = datetime.now() queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp) - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate)) test_lcp.correct_map.update(cmap) # Queue state only tracks up to second - latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp,'%Y%m%d%H%M%S'),'%Y%m%d%H%M%S') + latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp, dateformat), dateformat) self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp) From 377b09e170fc7aa21aef6a2b021580602d567cda Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 27 Aug 2012 20:07:54 -0400 Subject: [PATCH 63/90] Adjust comments --- common/lib/xmodule/xmodule/capa_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 27641af3b5..cfe3d2d48b 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -462,14 +462,14 @@ class CapaModule(XModule): self.system.track_function('save_problem_check_fail', event_info) raise NotFoundError('Problem must be reset before it can be checked again') - # Problem queued. Students must wait XQUEUE_WAITTIME_BETWEEN_REQUESTS + # Problem queued. Students must wait a specified waittime before they are allowed to submit if self.lcp.is_queued(): current_time = datetime.datetime.now() prev_submit_time = self.lcp.get_recentmost_queuetime() waittime_between_requests = self.system.xqueue['waittime'] if (current_time-prev_submit_time).total_seconds() < waittime_between_requests: msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests - return {'success': msg, 'html': ''} + return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback try: old_state = self.lcp.get_state() From 8ed9ab44e74c82cccda8d85546234be9c22d4db6 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 27 Aug 2012 23:48:42 -0700 Subject: [PATCH 64/90] Contextualizing javascript parameters --- common/lib/capa/capa/responsetypes.py | 38 ++++++++++--------- .../xmodule/js/src/capa/display.coffee | 3 ++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b2d56b48ca..0e14ac46c1 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -384,19 +384,23 @@ class JavascriptResponse(LoncapaResponse): node_path = self.system.node_path + ":" + os.path.normpath(js_dir) tmp_env["NODE_PATH"] = node_path return tmp_env + + def call_node(self, args): + + subprocess_args = ["node"] + subprocess_args.extend(args) + + return subprocess.check_output(subprocess_args, env=self.get_node_env()) def generate_problem_state(self): generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js' - output = subprocess.check_output(["node", - generator_file, - self.generator, - json.dumps(self.generator_dependencies), - json.dumps(str(self.system.seed)), - json.dumps(self.params) - ], - env=self.get_node_env()).strip() + output = self.call_node([generator_file, + self.generator, + json.dumps(self.generator_dependencies), + json.dumps(str(self.system.seed)), + json.dumps(self.params)]).strip() return json.loads(output) @@ -407,7 +411,8 @@ class JavascriptResponse(LoncapaResponse): for param in self.xml.xpath('//*[@id=$id]//responseparam', id=self.xml.get('id')): - params[param.get("name")] = json.loads(param.get("value")) + raw_param = param.get("value") + params[param.get("name")] = contextualize_text(raw_param, self.context) return params @@ -442,15 +447,12 @@ class JavascriptResponse(LoncapaResponse): submission = json.dumps(None) grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js' - outputs = subprocess.check_output(["node", - grader_file, - self.grader, - json.dumps(self.grader_dependencies), - submission, - json.dumps(self.problem_state), - json.dumps(self.params) - ], - env=self.get_node_env()).split('\n') + outputs = self.call_node([grader_file, + self.grader, + json.dumps(self.grader_dependencies), + submission, + json.dumps(self.problem_state), + json.dumps(self.params)]).split('\n') all_correct = json.loads(outputs[0].strip()) evaluation = outputs[1].strip() diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 23fa4d70fe..27d3585f96 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -293,6 +293,9 @@ class @Problem problemState = data.data("problem_state") displayClass = window[data.data('display_class')] + if evaluation == '' + evaluation = null + container = $(element).find(".javascriptinput_container") submissionField = $(element).find(".javascriptinput_input") From 3842c39f8a3e5c7fc1c55681310cfc3ff1274e40 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 27 Aug 2012 23:52:27 -0700 Subject: [PATCH 65/90] Fixing order that problem scores are displayed in --- lms/djangoapps/courseware/grades.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 1af3ab1bda..7f28f3ca5c 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -17,6 +17,7 @@ log = logging.getLogger("mitx.courseware") def yield_module_descendents(module): stack = module.get_display_items() + stack.reverse() while len(stack) > 0: next_module = stack.pop() From f7ebc4dd23f1f6c0700902feaac7e68a2e98fc02 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 28 Aug 2012 00:49:32 -0700 Subject: [PATCH 66/90] Load params from json --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0e14ac46c1..4d24fc6c23 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -412,7 +412,7 @@ class JavascriptResponse(LoncapaResponse): id=self.xml.get('id')): raw_param = param.get("value") - params[param.get("name")] = contextualize_text(raw_param, self.context) + params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context)) return params From 02e076b7250ecf09837428a4812dc8b890c35d5f Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 28 Aug 2012 05:49:36 -0700 Subject: [PATCH 67/90] adding pygraphviz --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 72b13e63ba..3376fc1a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,4 +46,5 @@ django-sekizai<0.7 django-mptt>=0.5.3 sorl-thumbnail networkx +pygraphviz -r repo-requirements.txt From ad745aa9bdfc0589631d2a4693a2818a5dc1d1b7 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 28 Aug 2012 05:53:33 -0700 Subject: [PATCH 68/90] pygraphviz requires libgraphviz-dev on ubuntu --- create-dev-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create-dev-env.sh b/create-dev-env.sh index 96f212c9b5..3664129775 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -105,7 +105,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz" +APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz libgraphviz-dev" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" From 947ec74afc0cdc76b594d25edb6214c7ce912724 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 28 Aug 2012 06:13:41 -0700 Subject: [PATCH 69/90] Let JS responses return >1 point --- common/lib/capa/capa/responsetypes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b2d56b48ca..d721322d6b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -435,7 +435,11 @@ class JavascriptResponse(LoncapaResponse): (all_correct, evaluation, solution) = self.run_grader(json_submission) self.solution = solution correctness = 'correct' if all_correct else 'incorrect' - return CorrectMap(self.answer_id, correctness, msg=evaluation) + if all_correct: + points = self.get_max_score() + else: + points = 0 + return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation) def run_grader(self, submission): if submission is None or submission == '': From 024b9db10dfb0d462488cdda9f252ef2fd8f9417 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 28 Aug 2012 09:27:27 -0400 Subject: [PATCH 70/90] Log errors in external grader message parsing --- common/lib/capa/capa/responsetypes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index e7d7406cbd..620152be57 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1210,13 +1210,13 @@ class CodeResponse(LoncapaResponse): try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict") + log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg) return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict") + log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result) return fail for tag in ['correct', 'score', 'msg']: - if not score_result.has_key(tag): + if tag not in score_result: log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'") return fail From bd2374b6fe6dd0ddbecc460f2166891a76c4ee23 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 28 Aug 2012 10:22:08 -0400 Subject: [PATCH 71/90] Remove trailing slash from ajax url in ModuleSystem * also add a check for valid location in modx_dispatch --- lms/djangoapps/courseware/module_render.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 6c3db9bc18..e238e9ca06 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -198,6 +198,8 @@ def _get_module(user, request, location, student_module_cache, course_id, positi location=descriptor.location.url(), dispatch=''), ) + # Intended use is as {ajax_url}/{dispatch_command}, so get rid of the trailing slash. + ajax_url = ajax_url.rstrip('/') # Fully qualified callback URL for external queueing system xqueue_callback_url = '{proto}://{host}'.format( @@ -410,6 +412,10 @@ def modx_dispatch(request, dispatch, location, course_id): ''' # ''' (fix emacs broken parsing) + # Check parameters and fail fast if there's a problem + if not Location.is_valid(location): + raise Http404("Invalid location") + # Check for submitted files and basic file size checks p = request.POST.copy() if request.FILES: From 5c17366cd54fe9ef0552d78e4ace94bc0f01d82a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 28 Aug 2012 12:01:05 -0400 Subject: [PATCH 72/90] Add a wsgi file for the lms that loads the modulestores immediately after the application starts --- lms/wsgi.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lms/wsgi.py diff --git a/lms/wsgi.py b/lms/wsgi.py new file mode 100644 index 0000000000..270b019add --- /dev/null +++ b/lms/wsgi.py @@ -0,0 +1,14 @@ +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.envs.aws") + +# This application object is used by the development server +# as well as any WSGI server configured to use this file. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +from django.conf import settings +from xmodule.modulestore.django import modulestore + +for store_name in settings.MODULESTORE: + modulestore(store_name) From 44cff00f50d0807c1777358e65cbaff131853067 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Tue, 28 Aug 2012 12:20:36 -0400 Subject: [PATCH 73/90] Fix display titles on video pages --- lms/templates/video.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/templates/video.html b/lms/templates/video.html index 93273ddb87..fcf25f3d69 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -1,5 +1,5 @@ -% if name is not UNDEFINED and name is not None: -

${display_name}

+% if display_name is not UNDEFINED and display_name is not None: +

${display_name}

% endif
From eb48cae8875692cdd3a0608ce3145b0d2f265887 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 28 Aug 2012 12:50:48 -0400 Subject: [PATCH 74/90] Consolidate the timestamp uses in CodeResponse --- common/lib/capa/capa/responsetypes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index e10c124d55..7bd9106b88 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1127,11 +1127,14 @@ class CodeResponse(LoncapaResponse): # 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) + str(time.time()) + + 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'], @@ -1147,9 +1150,8 @@ class CodeResponse(LoncapaResponse): contents = self.payload.copy() # Metadata related to the student submission revealed to the external grader - current_time = datetime.now() student_info = {'anonymous_student_id': anonymous_student_id, - 'submission_time': str(current_time), + 'submission_time': qtime, } contents.update({'student_info': json.dumps(student_info)}) @@ -1165,7 +1167,6 @@ class CodeResponse(LoncapaResponse): body=json.dumps(contents)) # State associated with the queueing request - qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) queuestate = {'key': queuekey, 'time': qtime, } From 4970d9d9f9c331bcccd424170fb1329ab6c4b0f7 Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 28 Aug 2012 12:55:04 -0400 Subject: [PATCH 75/90] Don't need time import, have datetime --- common/lib/capa/capa/responsetypes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 7bd9106b88..4b3cde9d73 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -16,7 +16,6 @@ import numpy import random import re import requests -import time import traceback import hashlib import abc From fa074277cf56b0138356ab8a09132e77e24da2ff Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 28 Aug 2012 12:59:30 -0400 Subject: [PATCH 76/90] Adjust CodeResponse comments --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 4b3cde9d73..c5c6c895a4 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1010,7 +1010,7 @@ class CodeResponse(LoncapaResponse): ''' Grade student code using an external queueing server, called 'xqueue' - Expects 'xqueue' dict in ModuleSystem with the following keys: + Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, 'callback_url': Per-StudentModule callback URL where results are posted (string), 'default_queuename': Default queuename to submit request (string) From bad24df84274e6be75fa0b38cae0a21bcc7a2f4b Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 28 Aug 2012 13:31:06 -0400 Subject: [PATCH 77/90] Adjust tests to reflect XML requirement for external grader messages --- common/lib/xmodule/xmodule/tests/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 7697c96214..f95a92397a 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -285,6 +285,7 @@ class StringResponseWithHintTest(unittest.TestCase): class CodeResponseTest(unittest.TestCase): ''' Test CodeResponse + TODO: Add tests for external grader messages ''' @staticmethod def make_queuestate(key, time): @@ -318,6 +319,7 @@ class CodeResponseTest(unittest.TestCase): self.assertEquals(test_lcp.is_queued(), True) + def test_update_score(self): ''' Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem @@ -336,8 +338,9 @@ class CodeResponseTest(unittest.TestCase): old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) # Message format common to external graders - correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'}) - incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'}) + grader_msg = 'MESSAGE' # Must be valid XML + correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg': grader_msg}) + incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg': grader_msg}) xserver_msgs = {'correct': correct_score_msg, 'incorrect': incorrect_score_msg,} @@ -362,7 +365,7 @@ class CodeResponseTest(unittest.TestCase): new_cmap = CorrectMap() new_cmap.update(old_cmap) npoints = 1 if correctness=='correct' else 0 - new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None) + new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None) test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) From 6a6b24af79bc7da0806225a08b31fe063d1ca79c Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Tue, 28 Aug 2012 12:40:49 -0700 Subject: [PATCH 78/90] Disable jsresponse file compilation; done manually for now. --- common/lib/capa/capa/responsetypes.py | 51 +++++++++++-------- .../js/compiled/javascriptresponse.js | 50 ++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 common/lib/xmodule/xmodule/tests/test_files/js/compiled/javascriptresponse.js diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6655d3cb94..273b1a3f8f 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -318,30 +318,37 @@ class JavascriptResponse(LoncapaResponse): def compile_display_javascript(self): - latestTimestamp = 0 - basepath = self.system.filestore.root_path + '/js/' - for filename in (self.display_dependencies + [self.display]): - filepath = basepath + filename - timestamp = os.stat(filepath).st_mtime - if timestamp > latestTimestamp: - latestTimestamp = timestamp - - h = hashlib.md5() - h.update(self.answer_id + str(self.display_dependencies)) - compiled_filename = 'compiled/' + h.hexdigest() + '.js' - compiled_filepath = basepath + compiled_filename + # TODO FIXME + # arjun: removing this behavior for now (and likely forever). Keeping + # until we decide on exactly how to solve this issue. For now, files are + # manually being compiled to DATA_DIR/js/compiled. - if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp: - outfile = open(compiled_filepath, 'w') - for filename in (self.display_dependencies + [self.display]): - filepath = basepath + filename - infile = open(filepath, 'r') - outfile.write(infile.read()) - outfile.write(';\n') - infile.close() - outfile.close() + #latestTimestamp = 0 + #basepath = self.system.filestore.root_path + '/js/' + #for filename in (self.display_dependencies + [self.display]): + # filepath = basepath + filename + # timestamp = os.stat(filepath).st_mtime + # if timestamp > latestTimestamp: + # latestTimestamp = timestamp + # + #h = hashlib.md5() + #h.update(self.answer_id + str(self.display_dependencies)) + #compiled_filename = 'compiled/' + h.hexdigest() + '.js' + #compiled_filepath = basepath + compiled_filename - self.display_filename = compiled_filename + #if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp: + # outfile = open(compiled_filepath, 'w') + # for filename in (self.display_dependencies + [self.display]): + # filepath = basepath + filename + # infile = open(filepath, 'r') + # outfile.write(infile.read()) + # outfile.write(';\n') + # infile.close() + # outfile.close() + + # TODO this should also be fixed when the above is fixed. + filename = self.system.ajax_url.split('/')[-1] + '.js' + self.display_filename = 'compiled/' + filename def parse_xml(self): self.generator_xml = self.xml.xpath('//*[@id=$id]//generator', diff --git a/common/lib/xmodule/xmodule/tests/test_files/js/compiled/javascriptresponse.js b/common/lib/xmodule/xmodule/tests/test_files/js/compiled/javascriptresponse.js new file mode 100644 index 0000000000..6670c6a09a --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_files/js/compiled/javascriptresponse.js @@ -0,0 +1,50 @@ +// Generated by CoffeeScript 1.3.3 +(function() { + var MinimaxProblemDisplay, root, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + MinimaxProblemDisplay = (function(_super) { + + __extends(MinimaxProblemDisplay, _super); + + function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) { + this.state = state; + this.submission = submission; + this.evaluation = evaluation; + this.container = container; + this.submissionField = submissionField; + this.parameters = parameters != null ? parameters : {}; + MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters); + } + + MinimaxProblemDisplay.prototype.render = function() {}; + + MinimaxProblemDisplay.prototype.createSubmission = function() { + var id, value, _ref, _results; + this.newSubmission = {}; + if (this.submission != null) { + _ref = this.submission; + _results = []; + for (id in _ref) { + value = _ref[id]; + _results.push(this.newSubmission[id] = value); + } + return _results; + } + }; + + MinimaxProblemDisplay.prototype.getCurrentSubmission = function() { + return this.newSubmission; + }; + + return MinimaxProblemDisplay; + + })(XProblemDisplay); + + root = typeof exports !== "undefined" && exports !== null ? exports : this; + + root.TestProblemDisplay = TestProblemDisplay; + +}).call(this); +; From aa063d57295464d3eb31463c89e5bb492a78771d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 28 Aug 2012 18:18:37 -0400 Subject: [PATCH 79/90] [#35065403] Lets video player continue to work if subtitles cannot be loaded --- .../js/src/video/display/video_caption.coffee | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee index 1690fb6092..3094cc5119 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee @@ -1,4 +1,7 @@ class @VideoCaption extends Subview + initialize: -> + @loaded = false + bind: -> $(window).bind('resize', @resize) @$('.hide-subtitles').click @toggle @@ -24,6 +27,8 @@ class @VideoCaption extends Subview @captions = captions.text @start = captions.start + @loaded = true + if onTouchBasedDevice() $('.subtitles li').html "Caption will be displayed when you start playing the video." else @@ -47,37 +52,40 @@ class @VideoCaption extends Subview @rendered = true search: (time) -> - min = 0 - max = @start.length - 1 + if @loaded + min = 0 + max = @start.length - 1 - while min < max - index = Math.ceil((max + min) / 2) - if time < @start[index] - max = index - 1 - if time >= @start[index] - min = index - - return min + while min < max + index = Math.ceil((max + min) / 2) + if time < @start[index] + max = index - 1 + if time >= @start[index] + min = index + return min play: -> - @renderCaption() unless @rendered - @playing = true + if @loaded + @renderCaption() unless @rendered + @playing = true pause: -> - @playing = false + if @loaded + @playing = false updatePlayTime: (time) -> - # This 250ms offset is required to match the video speed - time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250) - newIndex = @search time + if @loaded + # This 250ms offset is required to match the video speed + time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250) + newIndex = @search time - if newIndex != undefined && @currentIndex != newIndex - if @currentIndex - @$(".subtitles li.current").removeClass('current') - @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + if newIndex != undefined && @currentIndex != newIndex + if @currentIndex + @$(".subtitles li.current").removeClass('current') + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') - @currentIndex = newIndex - @scrollCaption() + @currentIndex = newIndex + @scrollCaption() resize: => @$('.subtitles').css maxHeight: @captionHeight() From cbfc117e8bdba6a62ee3ce638184489a4a8affe6 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sun, 26 Aug 2012 22:04:49 -0400 Subject: [PATCH 80/90] a few edits to the xml format docs --- doc/xml-format.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/xml-format.md b/doc/xml-format.md index 256d4839bf..3994a23c5d 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -148,7 +148,7 @@ That's basically all there is to the organizational structure. Read the next se * `problem` -- a problem. See elsewhere in edx4edx for documentation on the format. * `problemset` -- logically, a series of related problems. Currently displayed vertically. May contain explanatory html, videos, etc. * `sequential` -- a sequence of content, currently displayed with a horizontal list of tabs. If possible, use a more semantically meaningful tag (currently, we only have `videosequence`). -* `vertical` -- a sequence of content, displayed vertically. If possible, use a more semantically meaningful tag (currently, we only have `problemset`). +* `vertical` -- a sequence of content, displayed vertically. Content will be accessed all at once, on the right part of the page. No navigational bar. May have to use browser scroll bars. Content split with separators. If possible, use a more semantically meaningful tag (currently, we only have `problemset`). * `video` -- a link to a video, currently expected to be hosted on youtube. * `videosequence` -- a sequence of videos. This can contain various non-video content; it just signals to the system that this is logically part of an explanatory sequence of content, as opposed to say an exam sequence. @@ -223,18 +223,23 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}. __Not inherited:__ * `display_name` - name that will appear when this content is displayed in the courseware. Useful for all tag types. -* `format` - subheading under display name -- currently only displayed for chapter sub-sections. +* `format` - subheading under display name -- currently only displayed for chapter sub-sections. Also used by the the grader to know how to process students assessments that the + section contains. New formats can be defined as a 'type' in the GRADER variable in course_settings.json. Optional. (TODO: double check this--what's the current behavior?) * `hide_from_toc` -- If set to true for a chapter or chapter subsection, will hide that element from the courseware navigation accordion. This is useful if you'd like to link to the content directly instead (e.g. for tutorials) * `ispublic` -- specify whether the course is public. You should be able to use start dates instead (?) __Inherited:__ * `start` -- when this content should be shown to students. Note that anyone with staff access to the course will always see everything. -* `showanswer` - only for psets, is binary (closed/open). -* `graded` - Tutorial vs. grade, again binary (true/false). If true, will be used in calculation of student grade. -* `rerandomise` - Provide different numbers/variables for problems to prevent cheating. Provide different answers from questions bank? -* `due` - Due date for assignment. Assignment will be closed after that. This is a very important function of a policy file. -* `graceperiod` - +* `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. +* `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". +* `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) + 'never' (all students see the same version of the problem) + 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) + Default: 'always'. Optional. +* `due` - Due date for assignment. Assignment will be closed after that. Values: valid date. Default: none. Optional. +* attempts: Number of allowed attempts. Values: integer. Default: infinite. Optional. +* `graceperiod` - A default length of time that the problem is still accessible after the due date in the format "2 days 3 hours" or "1 day 15 minutes". Note, graceperiods are currently the easiest way to handle time zones. Due dates are all expressed in UCT. * `xqa_key` -- for integration with Ike's content QA server. -- should typically be specified at the course level. __Inheritance example:__ @@ -258,6 +263,7 @@ Metadata can also live in the xml files, but anything defined in the policy file - note, some xml attributes are not metadata. e.g. in `