Commit d0397762 by Vik Paruchuri

Merge branch 'feature/vik/studio-oe' into feature/vik/advanced-studio

Conflicts:
	cms/djangoapps/contentstore/views.py
parents 67b73242 9f020e22
...@@ -66,10 +66,13 @@ from cms.djangoapps.models.settings.course_metadata import CourseMetadata ...@@ -66,10 +66,13 @@ from cms.djangoapps.models.settings.course_metadata import CourseMetadata
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video', 'advanced'] COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video', 'advanced', 'openended']
ADVANCED_COMPONENT_TYPES = {
'openended' : ['combinedopenended', 'peergrading'],
'advanced' : ['advanced']
}
# advanced/beta components that can be enabled for all courses or per-course in the policy file
ADVANCED_COMPONENT_TYPES = []
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'enable_advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'enable_advanced_modules'
...@@ -287,25 +290,31 @@ def edit_unit(request, location): ...@@ -287,25 +290,31 @@ def edit_unit(request, location):
component_templates = defaultdict(list) component_templates = defaultdict(list)
# check if there are any advanced modules specified in the course policy # check if there are any advanced modules specified in the course policy
advanced_component_types = list(ADVANCED_COMPONENT_TYPES) advanced_component_types = ADVANCED_COMPONENT_TYPES
course_metadata = CourseMetadata.fetch(course.location) course_metadata = CourseMetadata.fetch(course.location)
advanced_component_types.extend(course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, [])) course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, {})
if isinstance(course_advanced_keys,dict):
advanced_component_types.update(course_advanced_keys)
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
for template in templates: for template in templates:
component_template = ( if template.location.category in COMPONENT_TYPES:
template.display_name, #This is a hack to create categories for different xmodules
template.location.url(), category = template.location.category
'markdown' in template.metadata, for key in ADVANCED_COMPONENT_TYPES:
'empty' in template.metadata if template.location.category in ADVANCED_COMPONENT_TYPES[key]:
) category = key
if template.location.category in COMPONENT_TYPES: break
component_templates[template.location.category].append(component_template)
elif template.location.category in advanced_component_types:
component_templates[ADVANCED_COMPONENT_CATEGORY].append(component_template)
# order of component types for display purposes component_templates[template.location.category].append((
component_template_types = [type for type in COMPONENT_TYPES if type in component_templates.keys()] template.display_name,
template.location.url(),
'markdown' in template.metadata,
'empty' in template.metadata
))
components = [ components = [
component.location.url() component.location.url()
......
...@@ -254,6 +254,14 @@ ...@@ -254,6 +254,14 @@
background: url(../img/html-icon.png) center no-repeat; background: url(../img/html-icon.png) center no-repeat;
} }
.large-openended-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-problem-icon.png) center no-repeat;
}
.large-textbook-icon { .large-textbook-icon {
display: inline-block; display: inline-block;
width: 100px; width: 100px;
......
...@@ -4,5 +4,5 @@ setup( ...@@ -4,5 +4,5 @@ setup(
name="capa", name="capa",
version="0.1", version="0.1",
packages=find_packages(exclude=["tests"]), 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): ...@@ -333,6 +333,12 @@ class CapaModule(XModule):
reset_button = False reset_button = False
save_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 # User submitted a problem, and hasn't reset. We don't want
# more submissions. # more submissions.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
...@@ -630,11 +636,11 @@ class CapaModule(XModule): ...@@ -630,11 +636,11 @@ class CapaModule(XModule):
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed() and not self.max_attempts==0:
event_info['failure'] = 'closed' event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
'error': "Problem is closed"} 'msg': "Problem is closed"}
# Problem submitted. Student should reset before saving # Problem submitted. Student should reset before saving
# again. # again.
...@@ -642,13 +648,16 @@ class CapaModule(XModule): ...@@ -642,13 +648,16 @@ class CapaModule(XModule):
event_info['failure'] = 'done' event_info['failure'] = 'done'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, 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 self.lcp.student_answers = answers
# TODO: should this be save_problem_fail? Looks like success to me... self.system.track_function('save_problem_success', event_info)
self.system.track_function('save_problem_fail', event_info) msg = "Your answers have been saved"
return {'success': True} 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): def reset_problem(self, get):
''' Changes problem state to unfinished -- removes student answers, ''' Changes problem state to unfinished -- removes student answers,
......
...@@ -4,9 +4,8 @@ from lxml import etree ...@@ -4,9 +4,8 @@ from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
from .editing_module import EditingDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule):
} }
self.child_descriptor = descriptors[version_index](self.system) 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, 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) instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data)
...@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule): ...@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule):
return self.child_module.display_name return self.child_module.display_name
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): class CombinedOpenEndedDescriptor(RawDescriptor):
""" """
Module for adding combined open ended questions Module for adding combined open ended questions
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/raw-edit.html"
module_class = CombinedOpenEndedModule module_class = CombinedOpenEndedModule
filename_extension = "xml" filename_extension = "xml"
...@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
has_score = True has_score = True
template_dir_name = "combinedopenended" 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 ...@@ -262,9 +262,8 @@ class @Problem
save: => save: =>
Logger.log 'problem_save', @answers Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) => $.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
if response.success saveMessage = response.msg
saveMessage = "Your answers have been saved but not graded. Hit 'Check' to grade them." @gentle_alert saveMessage
@gentle_alert saveMessage
@updateProgress response @updateProgress response
refreshMath: (event, element) => refreshMath: (event, element) =>
......
...@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module(): ...@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done' INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done' DONE = 'done'
#Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor, def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs): instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
...@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module(): ...@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module():
Output: rendered html Output: rendered html
""" """
context = self.get_context() 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 return html
def get_html_nonsystem(self): def get_html_nonsystem(self):
...@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module(): ...@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module():
Output: HTML rendered directly via Mako Output: HTML rendered directly via Mako
""" """
context = self.get_context() 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 return html
def get_html_base(self): def get_html_base(self):
...@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module(): ...@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module():
'task_name' : 'Scored Rubric', 'task_name' : 'Scored Rubric',
'class_name' : 'combined-rubric-container' '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} return {'html': html, 'success': True}
def get_legend(self, get): def get_legend(self, get):
...@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module(): ...@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module():
context = { context = {
'legend_list' : LEGEND_LIST, '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} return {'html': html, 'success': True}
def get_results(self, get): def get_results(self, get):
...@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module(): ...@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module():
'submission_id' : ri['submission_ids'][i], 'submission_id' : ri['submission_ids'][i],
} }
context_list.append(context) 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, 'context_list' : context_list,
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT, 'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
'human_grader_types' : HUMAN_GRADER_TYPE, 'human_grader_types' : HUMAN_GRADER_TYPE,
...@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module(): ...@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module():
'task_name' : "Feedback", 'task_name' : "Feedback",
'class_name' : "result-container", '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} return {'html': html, 'success': True}
def get_status_ajax(self, get): def get_status_ajax(self, get):
...@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module(): ...@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module():
'legend_list' : LEGEND_LIST, 'legend_list' : LEGEND_LIST,
'render_via_ajax' : render_via_ajax, '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 return status_html
......
...@@ -30,6 +30,8 @@ class RubricParsingError(Exception): ...@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
class CombinedOpenEndedRubric(object): class CombinedOpenEndedRubric(object):
TEMPLATE_DIR = "combinedopenended/openended"
def __init__ (self, system, view_only = False): def __init__ (self, system, view_only = False):
self.has_score = False self.has_score = False
self.view_only = view_only self.view_only = view_only
...@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object): ...@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
rubric_scores = [cat['score'] for cat in rubric_categories] rubric_scores = [cat['score'] for cat in rubric_categories]
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories) max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores) 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: 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, html = self.system.render_template(rubric_template,
{'categories': rubric_categories, {'categories': rubric_categories,
'has_score': self.has_score, 'has_score': self.has_score,
...@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object): ...@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
for grader_type in tuple[3]: for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type) 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, {'categories': rubric_categories,
'has_score': True, 'has_score': True,
'view_only': True, 'view_only': True,
......
...@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
</openended> </openended>
""" """
TEMPLATE_DIR = "combinedopenended/openended"
def setup_response(self, system, location, definition, descriptor): def setup_response(self, system, location, definition, descriptor):
""" """
Sets up the response type. Sets up the response type.
...@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores'] rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']: 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}) {'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'], 'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()), 'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback, 'feedback': feedback,
...@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@return: Rendered html @return: Rendered html
""" """
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50} 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 return html
def handle_ajax(self, dispatch, get, system): def handle_ajax(self, dispatch, get, system):
...@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload, 'accept_file_upload': self.accept_file_upload,
'eta_message' : eta_string, '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 return html
......
...@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children ...@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from capa.util import * from capa.util import *
from peer_grading_service import PeerGradingService from peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service import controller_query_service
from datetime import datetime from datetime import datetime
...@@ -106,8 +106,14 @@ class OpenEndedChild(object): ...@@ -106,8 +106,14 @@ class OpenEndedChild(object):
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = static_data['max_score'] self._max_score = static_data['max_score']
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system) if system.open_ended_grading_interface:
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system) 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 self.system = system
...@@ -461,11 +467,14 @@ class OpenEndedChild(object): ...@@ -461,11 +467,14 @@ class OpenEndedChild(object):
return success, allowed_to_submit, error_message return success, allowed_to_submit, error_message
def get_eta(self): def get_eta(self):
response = self.controller_qs.check_for_eta(self.location_string) if self.controller_qs:
try: response = self.controller_qs.check_for_eta(self.location_string)
response = json.loads(response) try:
except: response = json.loads(response)
pass except:
pass
else:
return ""
success = response['success'] success = response['success']
if isinstance(success, basestring): if isinstance(success, basestring):
......
...@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
</selfassessment> </selfassessment>
""" """
TEMPLATE_DIR = "combinedopenended/selfassessment"
def setup_response(self, system, location, definition, descriptor): def setup_response(self, system, location, definition, descriptor):
""" """
Sets up the module Sets up the module
...@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'accept_file_upload': self.accept_file_upload, '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 return html
...@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error #This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state)) 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): def get_hint_html(self, system):
""" """
...@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
#This is a dev_facing_error #This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state)) 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): def save_answer(self, get, system):
......
...@@ -14,7 +14,7 @@ from xmodule.modulestore import Location ...@@ -14,7 +14,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from timeinfo import TimeInfo 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__) log = logging.getLogger(__name__)
...@@ -53,13 +53,28 @@ class PeerGradingModule(XModule): ...@@ -53,13 +53,28 @@ class PeerGradingModule(XModule):
#We need to set the location here so the child modules can use it #We need to set the location here so the child modules can use it
system.set('location', location) system.set('location', location)
self.system = system 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) self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
if isinstance(self.use_for_single_location, basestring): if isinstance(self.use_for_single_location, basestring):
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT) 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) self.is_graded = self.metadata.get('is_graded', IS_GRADED)
if isinstance(self.is_graded, basestring): if isinstance(self.is_graded, basestring):
self.is_graded = (self.is_graded in TRUE_DICT) self.is_graded = (self.is_graded in TRUE_DICT)
...@@ -75,17 +90,6 @@ class PeerGradingModule(XModule): ...@@ -75,17 +90,6 @@ class PeerGradingModule(XModule):
self.display_due_date = self.timeinfo.display_due_date 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 self.ajax_url = self.system.ajax_url
if not self.ajax_url.endswith("/"): if not self.ajax_url.endswith("/"):
...@@ -452,6 +456,7 @@ class PeerGradingModule(XModule): ...@@ -452,6 +456,7 @@ class PeerGradingModule(XModule):
try: try:
problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id) problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id)
problem_list_dict = problem_list_json problem_list_dict = problem_list_json
log.debug(problem_list_dict)
success = problem_list_dict['success'] success = problem_list_dict['success']
if 'error' in problem_list_dict: if 'error' in problem_list_dict:
error_text = problem_list_dict['error'] error_text = problem_list_dict['error']
...@@ -467,6 +472,9 @@ class PeerGradingModule(XModule): ...@@ -467,6 +472,9 @@ class PeerGradingModule(XModule):
#This is a student_facing_error #This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff." error_text = "Could not get list of problems to peer grade. Please notify course staff."
success = False success = False
except:
log.exception("Could not contact peer grading service.")
success = False
def _find_corresponding_module_for_location(location): def _find_corresponding_module_for_location(location):
...@@ -562,7 +570,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -562,7 +570,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
""" """
Module for adding combined open ended questions Module for adding combined open ended questions
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/raw-edit.html"
module_class = PeerGradingModule module_class = PeerGradingModule
filename_extension = "xml" filename_extension = "xml"
...@@ -606,13 +614,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -606,13 +614,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.''' '''Return an xml element representing this definition.'''
elt = etree.Element('peergrading') 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 return elt
---
metadata:
display_name: Open Ended Response
rerandomize: never
showanswer: never
weight: ""
attempts: 10
max_score: 1
is_graded: False
version: 1
display_name: Test Question
data: |
<combinedopenended attempts="10" max_score="1" display_name = "Open Ended Response" is_graded="True" version="1">
<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: []
---
metadata:
display_name: Peer Grading Interface
rerandomize: never
showanswer: never
weight: ""
attempts: ""
data: |
<peergrading use_for_single_location="False" is_graded="False" display_name = "Peer Grading Panel">
</peergrading>
children: []
"""This file contains (or should), all access control logic for the courseware. """This file contains (or should), all access control logic for the courseware.
Ideally, it will be the only place that needs to know about any special settings Ideally, it will be the only place that needs to know about any special settings
like DISABLE_START_DATES""" like DISABLE_START_DATES"""
import logging import logging
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
...@@ -363,21 +363,15 @@ def _course_org_staff_group_name(location, course_context=None): ...@@ -363,21 +363,15 @@ def _course_org_staff_group_name(location, course_context=None):
return 'staff_%s' % course_id.split('/')[0] return 'staff_%s' % course_id.split('/')[0]
def _course_staff_group_name(location, course_context=None): def group_names_for(role, location, course_context=None):
""" """Returns the group names for a given role with this location. Plural
Get the name of the staff group for a location in the context of a course run. because it will return both the name we expect now as well as the legacy
group name we support for backwards compatibility. This should not check
location: something that can passed to Location the DB for existence of a group (like some of its callers do) because that's
course_context: A course_id that specifies the course run in which the location occurs. a DB roundtrip, and we expect this might be invoked many times as we crawl
Required if location doesn't have category 'course' an XModule tree."""
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location) loc = Location(location)
legacy_name = 'staff_%s' % loc.course legacy_group_name = '{0}_{1}'.format(role, loc.course)
if _does_course_group_name_exist(legacy_name):
return legacy_name
if loc.category == 'course': if loc.category == 'course':
course_id = loc.course_id course_id = loc.course_id
...@@ -386,22 +380,31 @@ def _course_staff_group_name(location, course_context=None): ...@@ -386,22 +380,31 @@ def _course_staff_group_name(location, course_context=None):
raise CourseContextRequired() raise CourseContextRequired()
course_id = course_context course_id = course_context
return 'staff_%s' % course_id group_name = '{0}_{1}'.format(role, course_id)
return [group_name, legacy_group_name]
def course_beta_test_group_name(location): group_names_for_staff = partial(group_names_for, 'staff')
group_names_for_instructor = partial(group_names_for, 'instructor')
def _course_staff_group_name(location, course_context=None):
""" """
Get the name of the beta tester group for a location. Right now, that's Get the name of the staff group for a location in the context of a course run.
beta_testers_COURSE.
location: something that can passed to Location. location: something that can passed to Location
course_context: A course_id that specifies the course run in which the location occurs.
Required if location doesn't have category 'course'
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
""" """
return 'beta_testers_{0}'.format(Location(location).course) loc = Location(location)
group_name, legacy_group_name = group_names_for_staff(location, course_context)
# nosetests thinks that anything with _test_ in the name is a test. if _does_course_group_name_exist(legacy_group_name):
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html) return legacy_group_name
course_beta_test_group_name.__test__ = False
return group_name
def _course_org_instructor_group_name(location, course_context=None): def _course_org_instructor_group_name(location, course_context=None):
""" """
...@@ -437,18 +440,26 @@ def _course_instructor_group_name(location, course_context=None): ...@@ -437,18 +440,26 @@ def _course_instructor_group_name(location, course_context=None):
using course_id rather than just the course number. So first check to see if the group name exists using course_id rather than just the course number. So first check to see if the group name exists
""" """
loc = Location(location) loc = Location(location)
legacy_name = 'instructor_%s' % loc.course group_name, legacy_group_name = group_names_for_instructor(location, course_context)
if _does_course_group_name_exist(legacy_name):
return legacy_name
if loc.category == 'course': if _does_course_group_name_exist(legacy_group_name):
course_id = loc.course_id return legacy_group_name
else:
if course_context is None: return group_name
raise CourseContextRequired()
course_id = course_context def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
return 'instructor_%s' % course_id
def _has_global_staff_access(user): def _has_global_staff_access(user):
...@@ -540,23 +551,22 @@ def _has_access_to_location(user, location, access_level, course_context): ...@@ -540,23 +551,22 @@ def _has_access_to_location(user, location, access_level, course_context):
user_groups = [g.name for g in user.groups.all()] user_groups = [g.name for g in user.groups.all()]
if access_level == 'staff': if access_level == 'staff':
staff_group = _course_staff_group_name(location, course_context) staff_groups = group_names_for_staff(location, course_context) + \
# org_staff_group is a group for an entire organization [_course_org_staff_group_name(location, course_context)]
org_staff_group = _course_org_staff_group_name(location, course_context) for staff_group in staff_groups:
if staff_group in user_groups or org_staff_group in user_groups: if staff_group in user_groups:
debug("Allow: user in group %s", staff_group) debug("Allow: user in group %s", staff_group)
return True return True
debug("Deny: user not in group %s", staff_group) debug("Deny: user not in groups %s", staff_groups)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_group = _course_instructor_group_name(location, course_context) instructor_groups = group_names_for_instructor(location, course_context) + \
instructor_staff_group = _course_org_instructor_group_name( [_course_org_instructor_group_name(location, course_context)]
location, course_context) for instructor_group in instructor_groups:
if instructor_group in user_groups or instructor_staff_group in user_groups: if instructor_group in user_groups:
debug("Allow: user in group %s", instructor_group) debug("Allow: user in group %s", instructor_group)
return True return True
debug("Deny: user not in group %s", instructor_group) debug("Deny: user not in groups %s", instructor_groups)
else: else:
log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level) log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level)
......
...@@ -86,7 +86,8 @@ def render_accordion(request, course, chapter, section): ...@@ -86,7 +86,8 @@ def render_accordion(request, course, chapter, section):
Returns the html string''' Returns the html string'''
# grab the table of contents # grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section) user = User.objects.prefetch_related("groups").get(id=request.user.id)
toc = toc_for_course(user, request, course, chapter, section)
context = dict([('toc', toc), context = dict([('toc', toc),
('course_id', course.id), ('course_id', course.id),
...@@ -250,23 +251,24 @@ def index(request, course_id, chapter=None, section=None, ...@@ -250,23 +251,24 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse - HTTPresponse
""" """
course = get_course_with_access(request.user, course_id, 'load', depth=2) user = User.objects.prefetch_related("groups").get(id=request.user.id)
staff_access = has_access(request.user, course, 'staff') course = get_course_with_access(user, course_id, 'load', depth=2)
registered = registered_for_course(course, request.user) staff_access = has_access(user, course, 'staff')
registered = registered_for_course(course, user)
if not registered: if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course? # TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (request.user, course.location.url())) log.debug('User %s tried to view course %s but is not enrolled' % (user, course.location.url()))
return redirect(reverse('about_course', args=[course.id])) return redirect(reverse('about_course', args=[course.id]))
try: try:
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2) course.id, user, course, depth=2)
# Has this student been in this course before? # Has this student been in this course before?
first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
# Load the module for the course # Load the module for the course
course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id) course_module = get_module_for_descriptor(user, request, course, student_module_cache, course.id)
if course_module is None: if course_module is None:
log.warning('If you see this, something went wrong: if we got this' log.warning('If you see this, something went wrong: if we got this'
' far, should have gotten a course module for this user') ' far, should have gotten a course module for this user')
...@@ -288,7 +290,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -288,7 +290,7 @@ def index(request, course_id, chapter=None, section=None,
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter) chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
if chapter_descriptor is not None: if chapter_descriptor is not None:
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) instance_module = get_instance_module(course_id, user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module) save_child_position(course_module, chapter, instance_module)
else: else:
raise Http404('No chapter descriptor found with name {}'.format(chapter)) raise Http404('No chapter descriptor found with name {}'.format(chapter))
...@@ -307,9 +309,9 @@ def index(request, course_id, chapter=None, section=None, ...@@ -307,9 +309,9 @@ def index(request, course_id, chapter=None, section=None,
# Load all descendants of the section, because we're going to display its # Load all descendants of the section, because we're going to display its
# html, which in general will need all of its children # html, which in general will need all of its children
section_module_cache = StudentModuleCache.cache_for_descriptor_descendents( section_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, section_descriptor, depth=None) course.id, user, section_descriptor, depth=None)
section_module = get_module(request.user, request, section_descriptor.location, section_module = get_module(user, request, section_descriptor.location,
section_module_cache, course.id, position=position, depth=None) section_module_cache, course.id, position=position, depth=None)
if section_module is None: if section_module is None:
# User may be trying to be clever and access something # User may be trying to be clever and access something
...@@ -317,12 +319,12 @@ def index(request, course_id, chapter=None, section=None, ...@@ -317,12 +319,12 @@ def index(request, course_id, chapter=None, section=None,
raise Http404 raise Http404
# Save where we are in the chapter # Save where we are in the chapter
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) instance_module = get_instance_module(course_id, user, chapter_module, student_module_cache)
save_child_position(chapter_module, section, instance_module) save_child_position(chapter_module, section, instance_module)
# check here if this section *is* a timed module. # check here if this section *is* a timed module.
if section_module.category == 'timelimit': if section_module.category == 'timelimit':
timer_context = update_timelimit_module(request.user, course_id, student_module_cache, timer_context = update_timelimit_module(user, course_id, student_module_cache,
section_descriptor, section_module) section_descriptor, section_module)
if 'timer_expiration_duration' in timer_context: if 'timer_expiration_duration' in timer_context:
context.update(timer_context) context.update(timer_context)
...@@ -363,7 +365,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -363,7 +365,7 @@ def index(request, course_id, chapter=None, section=None,
log.exception("Error in index view: user={user}, course={course}," log.exception("Error in index view: user={user}, course={course},"
" chapter={chapter} section={section}" " chapter={chapter} section={section}"
"position={position}".format( "position={position}".format(
user=request.user, user=user,
course=course, course=course,
chapter=chapter, chapter=chapter,
section=section, section=section,
......
# Create your views here. # Create your views here.
import json import json
from datetime import datetime from datetime import datetime
from django.http import HttpResponse, Http404 from django.http import Http404
from mitxmako.shortcuts import render_to_response
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from django.contrib.auth.models import User
def dictfetchall(cursor): def dictfetchall(cursor):
'''Returns all rows from a cursor as a dict. '''Returns a list of all rows from a cursor as a column: result dict.
Borrowed from Django documentation''' Borrowed from Django documentation'''
desc = cursor.description desc = cursor.description
return [ table=[]
dict(zip([col[0] for col in desc], row)) table.append([col[0] for col in desc])
for row in cursor.fetchall() table = table + cursor.fetchall()
] print "Table: " + str(table)
return table
def SQL_query_to_list(cursor, query_string):
cursor.execute(query_string)
raw_result=dictfetchall(cursor)
print raw_result
return raw_result
def dashboard(request): def dashboard(request):
""" """
Quick hack to show staff enrollment numbers. This should be Slightly less hackish hack to show staff enrollment numbers and other
replaced with a real dashboard later. This version is a short-term simple queries.
bandaid for the next couple weeks.
All queries here should be indexed and simple. Mostly, this means don't
touch courseware_studentmodule, as tempting as it may be.
""" """
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
queries = [] # results are passed to the template. The template knows how to render
queries.append("select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc;") # two types of results: scalars and tables. Scalars should be represented
queries.append("select count(distinct user_id) as unique_students from student_courseenrollment;") # as "Visible Title": Value and tables should be lists of lists where each
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;") # inner list represents a single row of the table
results = {"scalars":{},"tables":{}}
# count how many users we have
results["scalars"]["Unique Usernames"]=User.objects.filter().count()
results["scalars"]["Activated Usernames"]=User.objects.filter(is_active=1).count()
# count how many enrollments we have
results["scalars"]["Total Enrollments Across All Courses"]=CourseEnrollment.objects.count()
# establish a direct connection to the database (for executing raw SQL)
from django.db import connection from django.db import connection
cursor = connection.cursor() cursor = connection.cursor()
results = []
for query in queries: # define the queries that will generate our user-facing tables
cursor.execute(query) # table queries need not take the form of raw SQL, but do in this case since
results.append(dictfetchall(cursor)) # the MySQL backend for django isn't very friendly with group by or distinct
table_queries = {}
table_queries["course enrollments"]= \
"select "+ \
"course_id as Course, "+ \
"count(user_id) as Students " + \
"from student_courseenrollment "+ \
"group by course_id "+ \
"order by students desc;"
table_queries["number of students in each number of classes"]= \
"select registrations as 'Registered for __ Classes' , "+ \
"count(registrations) as Users "+ \
"from (select count(user_id) as registrations "+ \
"from student_courseenrollment "+ \
"group by user_id) as registrations_per_user "+ \
"group by registrations;"
# add the result for each of the table_queries to the results object
for query in table_queries.keys():
cursor.execute(table_queries[query])
results["tables"][query] = SQL_query_to_list(cursor, table_queries[query])
context={"results":results}
return HttpResponse(json.dumps(results, indent=4)) return render_to_response("admin_dashboard.html",context)
<%namespace name='static' file='static_content.html'/>
<%inherit file="main.html" />
<section class="container about">
<section class="basic_stats">
<div class="edx_summary">
<h2>edX-wide Summary</h2>
<table style="margin-left:auto;margin-right:auto;width:50%">
% for key in results["scalars"]:
<tr>
<td>${key}</td>
<td>${results["scalars"][key]}</td>
</tr>
% endfor
</table>
</div>
% for table in results["tables"]:
<br/>
<div class="table_display">
<h2>${table}</h2>
<table style="margin-left:auto;margin-right:auto;width:50%">
<tr>
% for column in results["tables"][table][0]:
<td><b>${column}</b></td>
% endfor
</tr>
% for row in results["tables"][table][1:]:
<tr>
% for column in row:
<td>${column}</td>
% endfor
</tr>
% endfor
</table>
</div>
% endfor
</section>
</section>
...@@ -20,7 +20,7 @@ http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz ...@@ -20,7 +20,7 @@ http://sympy.googlecode.com/files/sympy-0.7.1.tar.gz
newrelic==1.8.0.13 newrelic==1.8.0.13
glob2==0.3 glob2==0.3
pymongo==2.4.1 pymongo==2.4.1
django_nose django_nose==1.1
nosexcover==1.0.7 nosexcover==1.0.7
rednose==0.3.3 rednose==0.3.3
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment