Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions common/lib/capa/capa/responsetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
CorrectMap = correctmap.CorrectMap # pylint: disable=invalid-name
CORRECTMAP_PY = None

# Make '_' a no-op so we can scrape strings
_ = lambda text: text

#-----------------------------------------------------------------------------
# Exceptions
Expand Down Expand Up @@ -439,6 +441,7 @@ class JavascriptResponse(LoncapaResponse):
Javascript using Node.js.
"""

human_name = _('Javascript Input')
tags = ['javascriptresponse']
max_inputfields = 1
allowed_inputfields = ['javascriptinput']
Expand Down Expand Up @@ -684,6 +687,7 @@ class ChoiceResponse(LoncapaResponse):

"""

human_name = _('Checkboxes')
tags = ['choiceresponse']
max_inputfields = 1
allowed_inputfields = ['checkboxgroup', 'radiogroup']
Expand Down Expand Up @@ -754,6 +758,7 @@ class MultipleChoiceResponse(LoncapaResponse):
"""
# TODO: handle direction and randomize

human_name = _('Multiple Choice')
tags = ['multiplechoiceresponse']
max_inputfields = 1
allowed_inputfields = ['choicegroup']
Expand Down Expand Up @@ -1042,6 +1047,7 @@ def sample_from_answer_pool(self, choices, rng, num_pool):
@registry.register
class TrueFalseResponse(MultipleChoiceResponse):

human_name = _('True/False Choice')
tags = ['truefalseresponse']

def mc_setup_response(self):
Expand Down Expand Up @@ -1073,6 +1079,7 @@ class OptionResponse(LoncapaResponse):
TODO: handle direction and randomize
"""

human_name = _('Dropdown')
tags = ['optionresponse']
hint_tag = 'optionhint'
allowed_inputfields = ['optioninput']
Expand Down Expand Up @@ -1108,6 +1115,7 @@ class NumericalResponse(LoncapaResponse):
to a number (e.g. `4+5/2^2`), and accepts with a tolerance.
"""

human_name = _('Numerical Input')
tags = ['numericalresponse']
hint_tag = 'numericalhint'
allowed_inputfields = ['textline', 'formulaequationinput']
Expand Down Expand Up @@ -1308,6 +1316,7 @@ class StringResponse(LoncapaResponse):
</hintgroup>
</stringresponse>
"""
human_name = _('Text Input')
tags = ['stringresponse']
hint_tag = 'stringhint'
allowed_inputfields = ['textline']
Expand Down Expand Up @@ -1426,6 +1435,7 @@ class CustomResponse(LoncapaResponse):
or in a <script>...</script>
"""

human_name = _('Custom Evaluated Script')
tags = ['customresponse']

allowed_inputfields = ['textline', 'textbox', 'crystallography',
Expand Down Expand Up @@ -1797,6 +1807,7 @@ class SymbolicResponse(CustomResponse):
Symbolic math response checking, using symmath library.
"""

human_name = _('Symbolic Math Input')
tags = ['symbolicresponse']
max_inputfields = 1

Expand Down Expand Up @@ -1865,6 +1876,7 @@ class CodeResponse(LoncapaResponse):

"""

human_name = _('Code Input')
tags = ['coderesponse']
allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
max_inputfields = 1
Expand Down Expand Up @@ -2142,6 +2154,7 @@ class ExternalResponse(LoncapaResponse):

"""

human_name = _('External Grader')
tags = ['externalresponse']
allowed_inputfields = ['textline', 'textbox']
awdmap = {
Expand Down Expand Up @@ -2299,6 +2312,7 @@ class FormulaResponse(LoncapaResponse):
Checking of symbolic math response using numerical sampling.
"""

human_name = _('Math Expression Input')
tags = ['formularesponse']
hint_tag = 'formulahint'
allowed_inputfields = ['textline', 'formulaequationinput']
Expand Down Expand Up @@ -2511,6 +2525,7 @@ class SchematicResponse(LoncapaResponse):
"""
Circuit schematic response type.
"""
human_name = _('Circuit Schematic Builder')
tags = ['schematicresponse']
allowed_inputfields = ['schematic']

Expand Down Expand Up @@ -2589,6 +2604,7 @@ class ImageResponse(LoncapaResponse):
True, if click is inside any region or rectangle. Otherwise False.
"""

human_name = _('Image Mapped Input')
tags = ['imageresponse']
allowed_inputfields = ['imageinput']

Expand Down Expand Up @@ -2707,6 +2723,7 @@ class AnnotationResponse(LoncapaResponse):
The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
"""
human_name = _('Annotation Input')
tags = ['annotationresponse']
allowed_inputfields = ['annotationinput']
max_inputfields = 1
Expand Down Expand Up @@ -2831,6 +2848,7 @@ class ChoiceTextResponse(LoncapaResponse):
ChoiceResponse.
"""

human_name = _('Checkboxes With Text Input')
tags = ['choicetextresponse']
max_inputfields = 1
allowed_inputfields = ['choicetextgroup',
Expand Down
9 changes: 9 additions & 0 deletions common/lib/xmodule/xmodule/capa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import json
import logging
import sys
from lxml import etree

from pkg_resources import resource_string

from .capa_base import CapaMixin, CapaFields, ComplexEncoder
from capa import responsetypes
from .progress import Progress
from xmodule.x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
Expand Down Expand Up @@ -172,6 +174,13 @@ def non_editable_metadata_fields(self):
])
return non_editable_fields

@property
def problem_types(self):
""" Low-level problem type introspection for content libraries filtering by problem type """
tree = etree.XML(self.data)
registered_tags = responsetypes.registry.registered_tags()
return set([node.tag for node in tree.iter() if node.tag in registered_tags])

# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
Expand Down
121 changes: 99 additions & 22 deletions common/lib/xmodule/xmodule/library_content_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from bson.objectid import ObjectId, InvalidId
from collections import namedtuple
from copy import copy
from capa.responsetypes import registry

from .mako_module import MakoModuleDescriptor
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import LibraryLocator
Expand All @@ -19,15 +21,38 @@
from .xml_module import XmlDescriptor
from pkg_resources import resource_string


# Make '_' a no-op so we can scrape strings
_ = lambda text: text


ANY_CAPA_TYPE_VALUE = 'any'


def enum(**enums):
""" enum helper in lieu of enum34 """
return type('Enum', (), enums)


def _get_human_name(problem_class):
"""
Get the human-friendly name for a problem type.
"""
return getattr(problem_class, 'human_name', problem_class.__name__)


def _get_capa_types():
"""
Gets capa types tags and labels
"""
capa_types = {tag: _get_human_name(registry.get_class_for_tag(tag)) for tag in registry.registered_tags()}

return [{'value': ANY_CAPA_TYPE_VALUE, 'display_name': _('Any Type')}] + sorted([
{'value': capa_type, 'display_name': caption}
for capa_type, caption in capa_types.items()
], key=lambda item: item.get('display_name'))


class LibraryVersionReference(namedtuple("LibraryVersionReference", "library_id version")):
"""
A reference to a specific library, with an optional version.
Expand Down Expand Up @@ -146,6 +171,13 @@ class LibraryContentFields(object):
default=1,
scope=Scope.settings,
)
capa_type = String(
display_name=_("Problem Type"),
help=_('Choose a problem type to fetch from the library. If "Any Type" is selected no filtering is applied.'),
default=ANY_CAPA_TYPE_VALUE,
values=_get_capa_types(),
scope=Scope.settings,
)
filters = String(default="") # TBD
has_score = Boolean(
display_name=_("Scored"),
Expand Down Expand Up @@ -311,6 +343,39 @@ def refresh_children(self, request, suffix, update_db=True): # pylint: disable=
lib_tools.update_children(self, user_id, update_db)
return Response()

def _validate_library_version(self, validation, lib_tools, version, library_key):
"""
Validates library version
"""
latest_version = lib_tools.get_library_version(library_key)
if latest_version is not None:
if version is None or version != latest_version:
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.WARNING,
_(u'This component is out of date. The library has new content.'),
action_class='library-update-btn', # TODO: change this to action_runtime_event='...' once the unit page supports that feature.
action_label=_(u"↻ Update now")
)
)
return False
else:
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.ERROR,
_(u'Library is invalid, corrupt, or has been deleted.'),
action_class='edit-button',
action_label=_(u"Edit Library List")
)
)
return False
return True

def _set_validation_error_if_empty(self, validation, summary):
""" Helper method to only set validation summary if it's empty """
if validation.empty:
validation.set_summary(summary)

def validate(self):
"""
Validates the state of this Library Content Module Instance. This
Expand All @@ -331,38 +396,50 @@ def validate(self):
)
return validation
lib_tools = self.runtime.service(self, 'library_tools')
matching_children_count = 0
for library_key, version in self.source_libraries:
latest_version = lib_tools.get_library_version(library_key)
if latest_version is not None:
if version is None or version != latest_version:
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.WARNING,
_(u'This component is out of date. The library has new content.'),
action_class='library-update-btn', # TODO: change this to action_runtime_event='...' once the unit page supports that feature.
action_label=_(u"↻ Update now")
)
)
break
else:
validation.set_summary(
StudioValidationMessage(
StudioValidationMessage.ERROR,
_(u'Library is invalid, corrupt, or has been deleted.'),
action_class='edit-button',
action_label=_(u"Edit Library List")
)
)
if not self._validate_library_version(validation, lib_tools, version, library_key):
break

library = lib_tools.get_library(library_key)
children_matching_filter = lib_tools.get_filtered_children(library, self.capa_type)
# get_filtered_children returns generator, so can't use len.
# And we don't actually need those children, so no point of constructing a list
matching_children_count += sum(1 for child in children_matching_filter)

if matching_children_count == 0:
self._set_validation_error_if_empty(
validation,
StudioValidationMessage(
StudioValidationMessage.WARNING,
_(u'There are no matching problem types in the specified libraries.'),
action_class='edit-button',
action_label=_(u"Select another problem type")
)
)

if matching_children_count < self.max_count:
self._set_validation_error_if_empty(
validation,
StudioValidationMessage(
StudioValidationMessage.WARNING,
_(u'The specified libraries are configured to fetch {count} problems, '
u'but there are only {actual} matching problems.')
.format(actual=matching_children_count, count=self.max_count),
action_class='edit-button',
action_label=_(u"Edit configuration")
)
)

return validation

def editor_saved(self, user, old_metadata, old_content):
"""
If source_libraries has been edited, refresh_children automatically.
"""
old_source_libraries = LibraryList().from_json(old_metadata.get('source_libraries', []))
if set(old_source_libraries) != set(self.source_libraries):
if set(old_source_libraries) != set(self.source_libraries) or \
old_metadata.get('capa_type', ANY_CAPA_TYPE_VALUE) != self.capa_type:
try:
self.refresh_children(None, None, update_db=False) # update_db=False since update_item() is about to be called anyways
except ValueError:
Expand Down
Loading