Merge branch 'feature/vik/advanced-studio' into feature/abarrett/annotatable_xmodule
This commit is contained in:
@@ -4,5 +4,5 @@ setup(
|
||||
name="capa",
|
||||
version="0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=['distribute', 'pyparsing'],
|
||||
install_requires=['distribute==0.6.34', 'pyparsing==1.5.6'],
|
||||
)
|
||||
|
||||
@@ -333,6 +333,12 @@ class CapaModule(XModule):
|
||||
reset_button = False
|
||||
save_button = False
|
||||
|
||||
# If attempts=0 then show just check and reset buttons; this is for survey questions using capa
|
||||
if self.max_attempts==0:
|
||||
check_button = False
|
||||
reset_button = True
|
||||
save_button = True
|
||||
|
||||
# User submitted a problem, and hasn't reset. We don't want
|
||||
# more submissions.
|
||||
if self.lcp.done and self.rerandomize == "always":
|
||||
@@ -630,11 +636,11 @@ class CapaModule(XModule):
|
||||
event_info['answers'] = answers
|
||||
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
if self.closed() and not self.max_attempts==0:
|
||||
event_info['failure'] = 'closed'
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Problem is closed"}
|
||||
'msg': "Problem is closed"}
|
||||
|
||||
# Problem submitted. Student should reset before saving
|
||||
# again.
|
||||
@@ -642,13 +648,16 @@ class CapaModule(XModule):
|
||||
event_info['failure'] = 'done'
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Problem needs to be reset prior to save."}
|
||||
'msg': "Problem needs to be reset prior to save"}
|
||||
|
||||
self.lcp.student_answers = answers
|
||||
|
||||
# TODO: should this be save_problem_fail? Looks like success to me...
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': True}
|
||||
self.system.track_function('save_problem_success', event_info)
|
||||
msg = "Your answers have been saved"
|
||||
if not self.max_attempts==0:
|
||||
msg += " but not graded. Hit 'Check' to grade them."
|
||||
return {'success': True,
|
||||
'msg': msg}
|
||||
|
||||
def reset_problem(self, get):
|
||||
''' Changes problem state to unfinished -- removes student answers,
|
||||
|
||||
@@ -4,9 +4,8 @@ from lxml import etree
|
||||
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from .editing_module import EditingDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule):
|
||||
}
|
||||
|
||||
self.child_descriptor = descriptors[version_index](self.system)
|
||||
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['xml_string']), self.system)
|
||||
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']), self.system)
|
||||
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
|
||||
instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data)
|
||||
|
||||
@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule):
|
||||
return self.child_module.display_name
|
||||
|
||||
|
||||
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
class CombinedOpenEndedDescriptor(RawDescriptor):
|
||||
"""
|
||||
Module for adding combined open ended questions
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
module_class = CombinedOpenEndedModule
|
||||
filename_extension = "xml"
|
||||
|
||||
@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
has_score = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Pull out the individual tasks, the rubric, and the prompt, and parse
|
||||
|
||||
Returns:
|
||||
{
|
||||
'rubric': 'some-html',
|
||||
'prompt': 'some-html',
|
||||
'task_xml': dictionary of xml strings,
|
||||
}
|
||||
"""
|
||||
|
||||
return {'xml_string' : etree.tostring(xml_object), 'metadata' : xml_object.attrib}
|
||||
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('combinedopenended')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['task']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
|
||||
@@ -262,9 +262,8 @@ class @Problem
|
||||
save: =>
|
||||
Logger.log 'problem_save', @answers
|
||||
$.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
|
||||
if response.success
|
||||
saveMessage = "Your answers have been saved but not graded. Hit 'Check' to grade them."
|
||||
@gentle_alert saveMessage
|
||||
saveMessage = response.msg
|
||||
@gentle_alert saveMessage
|
||||
@updateProgress response
|
||||
|
||||
refreshMath: (event, element) =>
|
||||
|
||||
@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module():
|
||||
INTERMEDIATE_DONE = 'intermediate_done'
|
||||
DONE = 'done'
|
||||
|
||||
#Where the templates live for this problem
|
||||
TEMPLATE_DIR = "combinedopenended"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
|
||||
|
||||
@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module():
|
||||
Output: rendered html
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = self.system.render_template('combined_open_ended.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def get_html_nonsystem(self):
|
||||
@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module():
|
||||
Output: HTML rendered directly via Mako
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = self.system.render_template('combined_open_ended.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def get_html_base(self):
|
||||
@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module():
|
||||
'task_name' : 'Scored Rubric',
|
||||
'class_name' : 'combined-rubric-container'
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_results.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_legend(self, get):
|
||||
@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module():
|
||||
context = {
|
||||
'legend_list' : LEGEND_LIST,
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_legend.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_results(self, get):
|
||||
@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module():
|
||||
'submission_id' : ri['submission_ids'][i],
|
||||
}
|
||||
context_list.append(context)
|
||||
feedback_table = self.system.render_template('open_ended_result_table.html', {
|
||||
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
|
||||
'context_list' : context_list,
|
||||
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
|
||||
'human_grader_types' : HUMAN_GRADER_TYPE,
|
||||
@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module():
|
||||
'task_name' : "Feedback",
|
||||
'class_name' : "result-container",
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_results.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_status_ajax(self, get):
|
||||
@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module():
|
||||
'legend_list' : LEGEND_LIST,
|
||||
'render_via_ajax' : render_via_ajax,
|
||||
}
|
||||
status_html = self.system.render_template("combined_open_ended_status.html", context)
|
||||
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
|
||||
|
||||
return status_html
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
|
||||
|
||||
class CombinedOpenEndedRubric(object):
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/openended"
|
||||
|
||||
def __init__ (self, system, view_only = False):
|
||||
self.has_score = False
|
||||
self.view_only = view_only
|
||||
@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
|
||||
rubric_scores = [cat['score'] for cat in rubric_categories]
|
||||
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
|
||||
max_score = max(max_scores)
|
||||
rubric_template = 'open_ended_rubric.html'
|
||||
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
|
||||
if self.view_only:
|
||||
rubric_template = 'open_ended_view_only_rubric.html'
|
||||
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
|
||||
html = self.system.render_template(rubric_template,
|
||||
{'categories': rubric_categories,
|
||||
'has_score': self.has_score,
|
||||
@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
|
||||
for grader_type in tuple[3]:
|
||||
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
|
||||
|
||||
html = self.system.render_template('open_ended_combined_rubric.html',
|
||||
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
|
||||
{'categories': rubric_categories,
|
||||
'has_score': True,
|
||||
'view_only': True,
|
||||
|
||||
@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
</openended>
|
||||
"""
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/openended"
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Sets up the response type.
|
||||
@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
rubric_scores = rubric_dict['rubric_scores']
|
||||
|
||||
if not response_items['success']:
|
||||
return system.render_template("open_ended_error.html",
|
||||
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
|
||||
{'errors': feedback})
|
||||
|
||||
feedback_template = system.render_template("open_ended_feedback.html", {
|
||||
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
|
||||
'grader_type': response_items['grader_type'],
|
||||
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
|
||||
'feedback': feedback,
|
||||
@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
@return: Rendered html
|
||||
"""
|
||||
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
|
||||
html = system.render_template('open_ended_evaluation.html', context)
|
||||
html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def handle_ajax(self, dispatch, get, system):
|
||||
@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
'accept_file_upload': self.accept_file_upload,
|
||||
'eta_message' : eta_string,
|
||||
}
|
||||
html = system.render_template('open_ended.html', context)
|
||||
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from capa.util import *
|
||||
from peer_grading_service import PeerGradingService
|
||||
from peer_grading_service import PeerGradingService, MockPeerGradingService
|
||||
import controller_query_service
|
||||
|
||||
from datetime import datetime
|
||||
@@ -106,8 +106,14 @@ class OpenEndedChild(object):
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
self._max_score = static_data['max_score']
|
||||
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
|
||||
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
|
||||
if system.open_ended_grading_interface:
|
||||
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
|
||||
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
|
||||
else:
|
||||
self.peer_gs = MockPeerGradingService()
|
||||
self.controller_qs = None
|
||||
|
||||
|
||||
|
||||
self.system = system
|
||||
|
||||
@@ -461,11 +467,14 @@ class OpenEndedChild(object):
|
||||
return success, allowed_to_submit, error_message
|
||||
|
||||
def get_eta(self):
|
||||
response = self.controller_qs.check_for_eta(self.location_string)
|
||||
try:
|
||||
response = json.loads(response)
|
||||
except:
|
||||
pass
|
||||
if self.controller_qs:
|
||||
response = self.controller_qs.check_for_eta(self.location_string)
|
||||
try:
|
||||
response = json.loads(response)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
return ""
|
||||
|
||||
success = response['success']
|
||||
if isinstance(success, basestring):
|
||||
|
||||
@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
</selfassessment>
|
||||
"""
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/selfassessment"
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Sets up the module
|
||||
@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
'accept_file_upload': self.accept_file_upload,
|
||||
}
|
||||
|
||||
html = system.render_template('self_assessment_prompt.html', context)
|
||||
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
|
||||
@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
#This is a dev_facing_error
|
||||
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
|
||||
|
||||
return system.render_template('self_assessment_rubric.html', context)
|
||||
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
|
||||
|
||||
def get_hint_html(self, system):
|
||||
"""
|
||||
@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
#This is a dev_facing_error
|
||||
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
|
||||
|
||||
return system.render_template('self_assessment_hint.html', context)
|
||||
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
|
||||
|
||||
|
||||
def save_answer(self, get, system):
|
||||
|
||||
@@ -14,7 +14,7 @@ from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from timeinfo import TimeInfo
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,13 +53,28 @@ class PeerGradingModule(XModule):
|
||||
#We need to set the location here so the child modules can use it
|
||||
system.set('location', location)
|
||||
self.system = system
|
||||
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
|
||||
if(self.system.open_ended_grading_interface):
|
||||
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
|
||||
else:
|
||||
self.peer_gs = MockPeerGradingService()
|
||||
|
||||
|
||||
self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
|
||||
if isinstance(self.use_for_single_location, basestring):
|
||||
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT)
|
||||
|
||||
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
|
||||
if self.use_for_single_location == True:
|
||||
try:
|
||||
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
|
||||
except:
|
||||
log.error("Linked location {0} for peer grading module {1} does not exist".format(
|
||||
self.link_to_location, self.location))
|
||||
raise
|
||||
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
|
||||
if due_date:
|
||||
self.metadata['due'] = due_date
|
||||
|
||||
self.is_graded = self.metadata.get('is_graded', IS_GRADED)
|
||||
if isinstance(self.is_graded, basestring):
|
||||
self.is_graded = (self.is_graded in TRUE_DICT)
|
||||
@@ -75,17 +90,6 @@ class PeerGradingModule(XModule):
|
||||
|
||||
self.display_due_date = self.timeinfo.display_due_date
|
||||
|
||||
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
|
||||
if self.use_for_single_location == True:
|
||||
try:
|
||||
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
|
||||
except:
|
||||
log.error("Linked location {0} for peer grading module {1} does not exist".format(
|
||||
self.link_to_location, self.location))
|
||||
raise
|
||||
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
|
||||
if due_date:
|
||||
self.metadata['due'] = due_date
|
||||
|
||||
self.ajax_url = self.system.ajax_url
|
||||
if not self.ajax_url.endswith("/"):
|
||||
@@ -467,6 +471,9 @@ class PeerGradingModule(XModule):
|
||||
#This is a student_facing_error
|
||||
error_text = "Could not get list of problems to peer grade. Please notify course staff."
|
||||
success = False
|
||||
except:
|
||||
log.exception("Could not contact peer grading service.")
|
||||
success = False
|
||||
|
||||
|
||||
def _find_corresponding_module_for_location(location):
|
||||
@@ -562,7 +569,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding combined open ended questions
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
module_class = PeerGradingModule
|
||||
filename_extension = "xml"
|
||||
|
||||
@@ -585,7 +592,6 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
'task_xml': dictionary of xml strings,
|
||||
}
|
||||
"""
|
||||
log.debug("In definition")
|
||||
expected_children = []
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) == 0:
|
||||
@@ -606,13 +612,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('peergrading')
|
||||
|
||||
def add_child(k):
|
||||
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
|
||||
child_node = etree.fromstring(child_str)
|
||||
elt.append(child_node)
|
||||
|
||||
for child in ['task']:
|
||||
add_child(child)
|
||||
|
||||
return elt
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Open Ended Response
|
||||
max_attempts: 1
|
||||
max_score: 1
|
||||
is_graded: False
|
||||
version: 1
|
||||
display_name: Open Ended Response
|
||||
skip_spelling_checks: False
|
||||
accept_file_upload: False
|
||||
data: |
|
||||
<combinedopenended>
|
||||
<rubric>
|
||||
<rubric>
|
||||
<category>
|
||||
<description>Category 1</description>
|
||||
<option>
|
||||
The response does not incorporate what is needed for a one response.
|
||||
</option>
|
||||
<option>
|
||||
The response is correct for category 1.
|
||||
</option>
|
||||
</category>
|
||||
</rubric>
|
||||
</rubric>
|
||||
<prompt>
|
||||
<p>Why is the sky blue?</p>
|
||||
</prompt>
|
||||
<task>
|
||||
<selfassessment/>
|
||||
</task>
|
||||
<task>
|
||||
<openended min_score_to_attempt="1" max_score_to_attempt="2">
|
||||
<openendedparam>
|
||||
<initial_display>Enter essay here.</initial_display>
|
||||
<answer_display>This is the answer.</answer_display>
|
||||
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"}</grader_payload>
|
||||
</openendedparam>
|
||||
</openended>
|
||||
</task>
|
||||
</combinedopenended>
|
||||
|
||||
|
||||
children: []
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Peer Grading Interface
|
||||
attempts: 1
|
||||
use_for_single_location: False
|
||||
link_to_location: None
|
||||
is_graded: False
|
||||
max_grade: 1
|
||||
data: |
|
||||
<peergrading>
|
||||
</peergrading>
|
||||
|
||||
children: []
|
||||
Reference in New Issue
Block a user