diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index ce5bf36559..49a609a879 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml -from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint from xmodule.modulestore.inheritance import own_metadata from xmodule.capa_module import CapaDescriptor @@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def test_edit_unit_full(self): self.check_edit_unit('full') + def _get_draft_counts(self, item): + cnt = 1 if getattr(item, 'is_draft', False) else 0 + for child in item.get_children(): + cnt = cnt + self._get_draft_counts(child) + + return cnt + + def test_get_depth_with_drafts(self): + import_from_xml(modulestore(), 'common/test/data/', ['simple']) + + course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', + 'course', '2012_Fall', None]), depth=None) + + # make sure no draft items have been returned + num_drafts = self._get_draft_counts(course) + self.assertEqual(num_drafts, 0) + + problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', + 'problem', 'ps01-simple', None])) + + # put into draft + modulestore('draft').clone_item(problem.location, problem.location) + + # make sure we can query that item and verify that it is a draft + draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', + 'problem', 'ps01-simple', None])) + self.assertTrue(getattr(draft_problem,'is_draft', False)) + + #now requery with depth + course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple', + 'course', '2012_Fall', None]), depth=None) + + # make sure just one draft item have been returned + num_drafts = self._get_draft_counts(course) + self.assertEqual(num_drafts, 1) + + def test_static_tab_reordering(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # check that there's actually content in the 'question' field self.assertGreater(len(items[0].question),0) + def test_xlint_fails(self): + err_cnt = perform_xlint('common/test/data', ['full']) + self.assertGreater(err_cnt, 0) + def test_delete(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index b6b8cd5023..bb7ac2bf06 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -1,3 +1,9 @@ +''' +Utilities for contentstore tests +''' + +#pylint: disable=W0603 + import json import copy from uuid import uuid4 @@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the TestCase and drops it they are finished. """ - def _pre_setup(self): - super(ModuleStoreTestCase, self)._pre_setup() + @staticmethod + def flush_mongo_except_templates(): + ''' + Delete everything in the module store except templates + ''' + modulestore = xmodule.modulestore.django.modulestore() + + # This query means: every item in the collection + # that is not a template + query = {"_id.course": {"$ne": "templates"}} + + # Remove everything except templates + modulestore.collection.remove(query) + + @staticmethod + def load_templates_if_necessary(): + ''' + Load templates into the modulestore only if they do not already exist. + We need the templates, because they are copied to create + XModules such as sections and problems + ''' + modulestore = xmodule.modulestore.django.modulestore() + + # Count the number of templates + query = {"_id.course": "templates"} + num_templates = modulestore.collection.find(query).count() + + if num_templates < 1: + update_templates() + + @classmethod + def setUpClass(cls): + ''' + Flush the mongo store and set up templates + ''' # Use a uuid to differentiate # the mongo collections on jenkins. - self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) - self.test_MODULESTORE = self.orig_MODULESTORE - self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - settings.MODULESTORE = self.test_MODULESTORE - - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" + cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE) + test_modulestore = cls.orig_modulestore + test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex + test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex xmodule.modulestore.django._MODULESTORES = {} - update_templates() + + settings.MODULESTORE = test_modulestore + + TestCase.setUpClass() + + @classmethod + def tearDownClass(cls): + ''' + Revert to the old modulestore settings + ''' + + # Clean up by dropping the collection + modulestore = xmodule.modulestore.django.modulestore() + modulestore.collection.drop() + + # Restore the original modulestore settings + settings.MODULESTORE = cls.orig_modulestore + + def _pre_setup(self): + ''' + Remove everything but the templates before each test + ''' + + # Flush anything that is not a template + ModuleStoreTestCase.flush_mongo_except_templates() + + # Check that we have templates loaded; if not, load them + ModuleStoreTestCase.load_templates_if_necessary() + + # Call superclass implementation + super(ModuleStoreTestCase, self)._pre_setup() def _post_teardown(self): - # Make sure you flush out the modulestore. - # Drop the collection at the end of the test, - # otherwise there will be lingering collections leftover - # from executing the tests. - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - settings.MODULESTORE = self.orig_MODULESTORE + ''' + Flush everything we created except the templates + ''' + # Flush anything that is not a template + ModuleStoreTestCase.flush_mongo_except_templates() + # Call superclass implementation super(ModuleStoreTestCase, self)._post_teardown() diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 62e45c51fd..83a2bde72d 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,11 +1,15 @@ +import logging from django.conf import settings from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from django.core.urlresolvers import reverse +import copy DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] +#In order to instantiate an open ended tab automatically, need to have this data +OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"} def get_modulestore(location): """ @@ -137,7 +141,7 @@ def compute_unit_state(unit): 'private' content is editabled and not visible in the LMS """ - if unit.cms.is_draft: + if getattr(unit, 'is_draft', False): try: modulestore('direct').get_item(unit.location) return UnitState.draft @@ -187,3 +191,35 @@ class CoursePageNames: SettingsGrading = "settings_grading" CourseOutline = "course_index" Checklists = "checklists" + +def add_open_ended_panel_tab(course): + """ + Used to add the open ended panel tab to a course if it does not exist. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs + course_tabs = copy.copy(course.tabs) + changed = False + #Check to see if open ended panel is defined in the course + if OPEN_ENDED_PANEL not in course_tabs: + #Add panel to the tabs if it is not defined + course_tabs.append(OPEN_ENDED_PANEL) + changed = True + return changed, course_tabs + +def remove_open_ended_panel_tab(course): + """ + Used to remove the open ended panel tab from a course if it exists. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs + course_tabs = copy.copy(course.tabs) + changed = False + #Check to see if open ended panel is defined in the course + if OPEN_ENDED_PANEL in course_tabs: + #Add panel to the tabs if it is not defined + course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL] + changed = True + return changed, course_tabs diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 229f36242b..8850f230eb 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -41,7 +41,7 @@ from xmodule.modulestore.mongo import MongoUsage from mitxmako.shortcuts import render_to_response, render_to_string from xmodule.modulestore.django import modulestore from xmodule_modifiers import replace_static_urls, wrap_xmodule -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from functools import partial from xmodule.contentstore.django import contentstore @@ -52,7 +52,8 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \ - UnitState, get_course_for_item, get_url_reverse + UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \ + remove_open_ended_panel_tab from xmodule.modulestore.xml_importer import import_from_xml from contentstore.course_info_model import get_course_updates, \ @@ -73,7 +74,8 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] -ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading'] +OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] +ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @@ -188,7 +190,7 @@ def course_index(request, org, course, name): 'coursename': name }) - course = modulestore().get_item(location) + course = modulestore().get_item(location, depth=3) sections = course.get_children() return render_to_response('overview.html', { @@ -208,19 +210,14 @@ def course_index(request, org, course, name): @login_required def edit_subsection(request, location): # check that we have permissions to edit this item - if not has_access(request.user, location): + course = get_course_for_item(location) + if not has_access(request.user, course.location): raise PermissionDenied() - item = modulestore().get_item(location) + item = modulestore().get_item(location, depth=1) - # TODO: we need a smarter way to figure out what course an item is in - for course in modulestore().get_courses(): - if (course.location.org == item.location.org and - course.location.course == item.location.course): - break - - lms_link = get_lms_link_for_item(location) - preview_link = get_lms_link_for_item(location, preview=True) + lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) + preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': @@ -253,12 +250,6 @@ def edit_subsection(request, location): can_view_live = True break - # item.lms.start is a struct_time using GMT - # item.lms.due is a String, 'March 20 17:00' - - # edit_subsection.html, due is converted to dateutil.parser.parse(item.lms.due) = {datetime} 2013-03-20 17:00:00 - #parsed_due_date = dateutil.parser.parse(item.lms.due) - return render_to_response('edit_subsection.html', {'subsection': item, 'context_course': course, @@ -283,19 +274,13 @@ def edit_unit(request, location): id: A Location URL """ - # check that we have permissions to edit this item - if not has_access(request.user, location): + course = get_course_for_item(location) + if not has_access(request.user, course.location): raise PermissionDenied() - item = modulestore().get_item(location) + item = modulestore().get_item(location, depth=1) - # TODO: we need a smarter way to figure out what course an item is in - for course in modulestore().get_courses(): - if (course.location.org == item.location.org and - course.location.course == item.location.course): - break - - lms_link = get_lms_link_for_item(item.location) + lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) component_templates = defaultdict(list) @@ -454,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, request.POST) + except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + except ProcessingError: + log.warning("Module raised an error while processing AJAX request", + exc_info=True) + return HttpResponseBadRequest() + except: log.exception("error processing ajax call") raise @@ -1279,15 +1271,48 @@ def course_advanced_updates(request, org, course, name): location = get_location_and_verify_access(request, org, course, name) real_method = get_request_method(request) - + if real_method == 'GET': return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") elif real_method == 'DELETE': - return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json") + return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), + mimetype="application/json") elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key - return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json") - + request_body = json.loads(request.body) + #Whether or not to filter the tabs key out of the settings metadata + filter_tabs = True + #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab + #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading + #module, and to remove it if they have removed the open ended elements. + if ADVANCED_COMPONENT_POLICY_KEY in request_body: + #Check to see if the user instantiated any open ended components + found_oe_type = False + #Get the course so that we can scrape current tabs + course_module = modulestore().get_item(location) + for oe_type in OPEN_ENDED_COMPONENT_TYPES: + if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Add an open ended tab to the course if needed + changed, new_tabs = add_open_ended_panel_tab(course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + #Set this flag to avoid the open ended tab removal code below. + found_oe_type = True + break + #If we did not find an open ended module type in the advanced settings, + # we may need to remove the open ended tab from the course. + if not found_oe_type: + #Remove open ended tab to the course if needed + changed, new_tabs = remove_open_ended_panel_tab(course_module) + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + return HttpResponse(response_json, mimetype="application/json") @ensure_csrf_cookie @login_required diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 563dd16524..70f69315ff 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor from xmodule.modulestore.inheritance import own_metadata from xblock.core import Scope from xmodule.course_module import CourseDescriptor - +import copy class CourseMetadata(object): ''' @@ -39,7 +39,7 @@ class CourseMetadata(object): return course @classmethod - def update_from_json(cls, course_location, jsondict): + def update_from_json(cls, course_location, jsondict, filter_tabs=True): """ Decode the json into CourseMetadata and save any changed attrs to the db. @@ -48,10 +48,16 @@ class CourseMetadata(object): descriptor = get_modulestore(course_location).get_item(course_location) dirty = False + + #Copy the filtered list to avoid permanently changing the class attribute + filtered_list = copy.copy(cls.FILTERED_LIST) + #Don't filter on the tab attribute if filter_tabs is False + if not filter_tabs: + filtered_list.remove("tabs") for k, v in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? - if k in cls.FILTERED_LIST: + if k in filtered_list: continue if hasattr(descriptor, k) and getattr(descriptor, k) != v: diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 5612db1396..b8d4d14b9e 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -142,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = { # To see stacktraces for MongoDB queries, set this to True. # Stacktraces slow down page loads drastically (for pages with lots of queries). -DEBUG_TOOLBAR_MONGO_STACKTRACES = False +DEBUG_TOOLBAR_MONGO_STACKTRACES = True diff --git a/cms/envs/test.py b/cms/envs/test.py index d7992cb471..59664bfd40 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -58,6 +58,10 @@ MODULESTORE = { 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'OPTIONS': modulestore_options + }, + 'draft': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': modulestore_options } } diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py index cad3110574..c9bb8f4c6e 100644 --- a/cms/xmodule_namespace.py +++ b/cms/xmodule_namespace.py @@ -40,7 +40,6 @@ class CmsNamespace(Namespace): """ Namespace with fields common to all blocks in Studio """ - is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings) published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_by = String(help="Id of the user who published this module", scope=Scope.settings) empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 68f80006f6..696b12377f 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -118,7 +118,7 @@ class LoncapaProblem(object): # 3. Assign from the OS's random number generator self.seed = state.get('seed', seed) if self.seed is None: - self.seed = struct.unpack('i', os.urandom(4)) + self.seed = struct.unpack('i', os.urandom(4))[0] self.student_answers = state.get('student_answers', {}) if 'correct_map' in state: self.correct_map.set_dict(state['correct_map']) diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index b726f765d8..950cd199fc 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -80,16 +80,17 @@ class CorrectMap(object): 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): - # empty current dict - self.__init__() - # create new dict entries + ''' + # empty current dict + self.__init__() + + # create new dict entries + if correct_map and not isinstance(correct_map.values()[0], dict): + # special migration for k in correct_map: - self.set(k, correct_map[k]) + self.set(k, correctness=correct_map[k]) else: - self.__init__() for k in correct_map: self.set(k, **correct_map[k]) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 2035c42661..5b1b46d858 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -17,6 +17,7 @@ import logging import numbers import numpy import os +import sys import random import re import requests @@ -52,12 +53,17 @@ class LoncapaProblemError(Exception): class ResponseError(Exception): ''' - Error for failure in processing a response + Error for failure in processing a response, including + exceptions that occur when executing a custom script. ''' pass class StudentInputError(Exception): + ''' + Error for an invalid student input. + For example, submitting a string when the problem expects a number + ''' pass #----------------------------------------------------------------------------- @@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse): import sys type, value, traceback = sys.exc_info() - raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" % + raise StudentInputError, ("Could not interpret '%s' as a number" % cgi.escape(student_answer)), traceback if correct: @@ -1072,13 +1078,10 @@ def sympy_check2(): correct = self.context['correct'] messages = self.context['messages'] overall_message = self.context['overall_message'] + except Exception as err: - print "oops in customresponse (code) error %s" % err - print "context = ", self.context - print traceback.format_exc() - # Notify student - raise StudentInputError( - "Error: Problem could not be evaluated with your input") + self._handle_exec_exception(err) + else: # self.code is not a string; assume its a function @@ -1105,13 +1108,9 @@ def sympy_check2(): nargs, args, kwargs)) ret = fn(*args[:nargs], **kwargs) + except Exception as err: - log.error("oops in customresponse (cfn) error %s" % err) - # print "context = ",self.context - log.error(traceback.format_exc()) - raise Exception("oops in customresponse (cfn) error %s" % err) - log.debug( - "[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret) + self._handle_exec_exception(err) if type(ret) == dict: @@ -1147,9 +1146,9 @@ def sympy_check2(): correct = [] messages = [] for input_dict in input_list: - correct.append('correct' + correct.append('correct' if input_dict['ok'] else 'incorrect') - msg = (self.clean_message_html(input_dict['msg']) + msg = (self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None) messages.append(msg) @@ -1157,7 +1156,7 @@ def sympy_check2(): # Raise an exception else: log.error(traceback.format_exc()) - raise Exception( + raise ResponseError( "CustomResponse: check function returned an invalid dict") # The check function can return a boolean value, @@ -1174,7 +1173,7 @@ def sympy_check2(): correct_map.set_overall_message(overall_message) for k in range(len(idset)): - npoints = (self.maxpoints[idset[k]] + npoints = (self.maxpoints[idset[k]] if correct[k] == 'correct' else 0) correct_map.set(idset[k], correct[k], msg=messages[k], npoints=npoints) @@ -1227,6 +1226,22 @@ def sympy_check2(): return {self.answer_ids[0]: self.expect} return self.default_answer_map + def _handle_exec_exception(self, err): + ''' + Handle an exception raised during the execution of + custom Python code. + + Raises a ResponseError + ''' + + # Log the error if we are debugging + msg = 'Error occurred while evaluating CustomResponse' + log.warning(msg, exc_info=True) + + # Notify student with a student input error + _, _, traceback_obj = sys.exc_info() + raise ResponseError, err.message, traceback_obj + #----------------------------------------------------------------------------- @@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse): submission = [json.loads(student_answers[ k]) for k in sorted(self.answer_ids)] self.context.update({'submission': submission}) - exec self.code in global_context, self.context + + try: + exec self.code in global_context, self.context + + except Exception as err: + _, _, traceback_obj = sys.exc_info() + raise ResponseError, ResponseError(err.message), traceback_obj + cmap = CorrectMap() cmap.set_dict(dict(zip(sorted( self.answer_ids), self.context['correct']))) @@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse): option_scoring = dict([(option['id'], { 'correctness': choices.get(option['choice']), 'points': scoring.get(option['choice']) - }) for option in self._find_options(inputfield) ]) + }) for option in self._find_options(inputfield)]) scoring_map[inputfield.get('id')] = option_scoring diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index 758e2ffba1..c9cc3fd28d 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -17,7 +17,7 @@ % for choice_id, choice_description in choices: