Commit 5029d3a3 by Arthur Barrett

Merge branch 'feature/vik/advanced-studio' into feature/abarrett/annotatable_xmodule

parents fd49aceb 102c594e
10664: Locked by 10664 at Mon Feb 11 14:22:22 -0500 2013
---
cursor_positions: []
files_to_retain: 0
ce76efcea5f0a5b2238364f81d54f1d393853a1a
\ No newline at end of file
...@@ -68,6 +68,10 @@ log = logging.getLogger(__name__) ...@@ -68,6 +68,10 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading']
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
# cdodge: these are categories which should not be parented, they are detached from the hierarchy # cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
...@@ -281,10 +285,31 @@ def edit_unit(request, location): ...@@ -281,10 +285,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. These modules
# should be specified as a list of strings, where the strings are the names of the modules
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
course_metadata = CourseMetadata.fetch(course.location)
course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, [])
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
if len(course_advanced_keys) > 0:
component_types.append(ADVANCED_COMPONENT_CATEGORY)
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:
if template.location.category in COMPONENT_TYPES: category = template.location.category
component_templates[template.location.category].append((
if category in course_advanced_keys:
category = ADVANCED_COMPONENT_CATEGORY
if category in component_types:
#This is a hack to create categories for different xmodules
component_templates[category].append((
template.display_name, template.display_name,
template.location.url(), template.location.url(),
'markdown' in template.metadata, 'markdown' in template.metadata,
......
...@@ -254,6 +254,30 @@ ...@@ -254,6 +254,30 @@
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-openended-icon.png) center no-repeat;
}
.large-annotations-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-annotations-icon.png) center no-repeat;
}
.large-advanced-icon {
display: inline-block;
width: 100px;
height: 60px;
margin-right: 5px;
background: url(../img/large-advanced-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("/"):
...@@ -467,6 +471,9 @@ class PeerGradingModule(XModule): ...@@ -467,6 +471,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 +569,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -562,7 +569,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"
...@@ -585,7 +592,6 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -585,7 +592,6 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
'task_xml': dictionary of xml strings, 'task_xml': dictionary of xml strings,
} }
""" """
log.debug("In definition")
expected_children = [] expected_children = []
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) == 0: if len(xml_object.xpath(child)) == 0:
...@@ -606,13 +612,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -606,13 +612,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
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: []
---
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: []
"""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)
...@@ -89,14 +89,24 @@ def peer_grading(request, course_id): ...@@ -89,14 +89,24 @@ def peer_grading(request, course_id):
Show a peer grading interface Show a peer grading interface
''' '''
#Get the current course
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
course_id_parts = course.id.split("/") course_id_parts = course.id.split("/")
course_id_norun = "/".join(course_id_parts[0:2]) false_dict = [False,"False", "false", "FALSE"]
pg_location = "i4x://" + course_id_norun + "/peergrading/init"
#Reverse the base course url
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: try:
problem_url_parts = search.path_to_location(modulestore(), course.id, pg_location) #TODO: This will not work with multiple runs of a course. Make it work. The last key in the Location passed
#to get_items is called revision. Is this the same as run?
#Get the peer grading modules currently in the course
items = modulestore().get_items(['i4x', None, course_id_parts[1], 'peergrading', None])
#See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if i.metadata.get("use_for_single_location", True) in false_dict]
#Get the first one
item_location = items[0].location
#Generate a url for the first module and redirect the user to it
problem_url_parts = search.path_to_location(modulestore(), course.id, item_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_url = generate_problem_url(problem_url_parts, base_course_url)
return HttpResponseRedirect(problem_url) return HttpResponseRedirect(problem_url)
......
<%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>
...@@ -21,12 +21,12 @@ ...@@ -21,12 +21,12 @@
<article class="response"> <article class="response">
<h3>What is edX?</h3> <h3>What is edX?</h3>
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p> <p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning both on-campus and online throughout the world.</p> <p>EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX , GeorgetownX and the University of Texas System classes online for free. The UT System includes nine universities and six health institutions. In 2014, edX will further expand its consortium, including several international schools, when it begins offering courses from École Polytechnique Fédérale de Lausanne, McGill University, University of Toronto, Australian National University, Delft University of Technology, and Rice University. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning both on-campus and online throughout the world.</p>
</article> </article>
<article class="response"> <article class="response">
<h3>Will edX be adding additional X Universities?</h3> <h3>Will edX be adding additional X Universities?</h3>
<p>More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities". Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.</p> <p>More than 200 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the "X University" Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley, the University of Texas system and the other consortium members will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities." As noted above, edX’s newest consortium members include Wellesley, Georgetown, École Polytechnique Fédérale de Lausanne, McGill University, University of Toronto, Australian National University, Delft University of Technology, and Rice University. Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities' educational content together on one site will enable learners worldwide to access the offered course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.</p>
<p>edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more "X Universities."</p> <p>edX will actively explore the addition of other institutions from around the world to the edX platform, and looks forward to adding more "X Universities."</p>
</article> </article>
</section> </section>
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
</article> </article>
<article class="response"> <article class="response">
<h3>What will the scope of the online courses be? How many? Which faculty?</h3> <h3>What will the scope of the online courses be? How many? Which faculty?</h3>
<p>Our goal is to offer a wide variety of courses across disciplines. There are currently <a href="/courses">nine courses</a> offered for Fall 2012.</p> <p>Our goal is to offer a wide variety of courses across disciplines. There are currently <a href="/courses">fifteen</a> offered on the edX platform.</p>
</article> </article>
<article class="response"> <article class="response">
<h3>Who is the learner? Domestic or international? Age range?</h3> <h3>Who is the learner? Domestic or international? Age range?</h3>
......
...@@ -299,6 +299,12 @@ ...@@ -299,6 +299,12 @@
</div> </div>
</article> </article>
<article class="response"> <article class="response">
<h3 class="question">I made a mistake creating my username – how do I fix it?</h3>
<div class ="answer" id="other_help_faq_answer_4.5">
<p>In most cases it would simplest and fastest to create a new account. Your old unused account will vanish naturally. If you were not aware of your mistake until much later, you should send us a detailed change request and we will do our best to edit your username. Please bear in mind that usernames are unique, and the one you want may be taken.</p>
</div>
</article>
<article class="response">
<h3 class="question">I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.</h3> <h3 class="question">I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.</h3>
<div class ="answer" id="other_help_faq_answer_5"> <div class ="answer" id="other_help_faq_answer_5">
<p>Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.</p> <p>Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.</p>
......
...@@ -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