Commit 51db547a by Diana Huang

Merge branch 'master' into diana/rubric-ui-improvements

Conflicts:
	common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
parents f3be02d7 8c4b6612
# .coveragerc for cms # .coveragerc for cms
[run] [run]
data_file = reports/cms/.coverage data_file = reports/cms/.coverage
source = cms source = cms,common/djangoapps
omit = cms/envs/*, cms/manage.py omit = cms/envs/*, cms/manage.py
[report] [report]
......
import logging import logging
from static_replace import replace_urls from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -18,7 +18,17 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link ...@@ -18,7 +18,17 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link
data = module.definition['data'] data = module.definition['data']
if rewrite_static_links: if rewrite_static_links:
data = replace_urls(module.definition['data'], course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])) data = replace_static_urls(
module.definition['data'],
None,
course_namespace=Location([
module.location.tag,
module.location.org,
module.location.course,
None,
None
])
)
return { return {
'id': module.location.url(), 'id': module.location.url(),
...@@ -47,7 +57,7 @@ def set_module_info(store, location, post_data): ...@@ -47,7 +57,7 @@ def set_module_info(store, location, post_data):
if post_data.get('data') is not None: if post_data.get('data') is not None:
data = post_data['data'] data = post_data['data']
store.update_item(location, data) store.update_item(location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array # cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually # so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection # deleting the children object from the children collection
......
...@@ -31,7 +31,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr ...@@ -31,7 +31,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from static_replace import replace_urls from static_replace import replace_static_urls
from external_auth.views import ssl_login_shortcut from external_auth.views import ssl_login_shortcut
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
...@@ -132,7 +132,7 @@ def has_access(user, location, role=STAFF_ROLE_NAME): ...@@ -132,7 +132,7 @@ def has_access(user, location, role=STAFF_ROLE_NAME):
Return True if user allowed to access this piece of data Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS, Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin) I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do has all the rights that STAFF do
...@@ -154,7 +154,7 @@ def course_index(request, org, course, name): ...@@ -154,7 +154,7 @@ def course_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
...@@ -213,7 +213,7 @@ def edit_subsection(request, location): ...@@ -213,7 +213,7 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI # remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems() policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
can_view_live = False can_view_live = False
...@@ -291,7 +291,7 @@ def edit_unit(request, location): ...@@ -291,7 +291,7 @@ def edit_unit(request, location):
containing_section = modulestore().get_item(containing_section_locs[0]) containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect # cdodge hack. We're having trouble previewing drafts via jump_to redirect
# so let's generate the link url here # so let's generate the link url here
# need to figure out where this item is in the list of children as the preview will need this # need to figure out where this item is in the list of children as the preview will need this
index =1 index =1
...@@ -302,12 +302,12 @@ def edit_unit(request, location): ...@@ -302,12 +302,12 @@ def edit_unit(request, location):
preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
preview='preview.', preview='preview.',
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
org=course.location.org, org=course.location.org,
course=course.location.course, course=course.location.course,
course_name=course.location.name, course_name=course.location.name,
section=containing_section.location.name, section=containing_section.location.name,
subsection=containing_subsection.location.name, subsection=containing_subsection.location.name,
index=index) index=index)
unit_state = compute_unit_state(item) unit_state = compute_unit_state(item)
...@@ -358,14 +358,14 @@ def assignment_type_update(request, org, course, category, name): ...@@ -358,14 +358,14 @@ def assignment_type_update(request, org, course, category, name):
location = Location(['i4x', org, course, category, name]) location = Location(['i4x', org, course, category, name])
if not has_access(request.user, location): if not has_access(request.user, location):
raise HttpResponseForbidden() raise HttpResponseForbidden()
if request.method == 'GET': if request.method == 'GET':
return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)), return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
mimetype="application/json") mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)), return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
mimetype="application/json") mimetype="application/json")
def user_author_string(user): def user_author_string(user):
'''Get an author string for commits by this user. Format: '''Get an author string for commits by this user. Format:
...@@ -473,7 +473,7 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -473,7 +473,7 @@ def preview_module_system(request, preview_id, descriptor):
get_module=partial(get_preview_module, request, preview_id), get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms, render_template=render_from_lms,
debug=True, debug=True,
replace_urls=replace_urls, replace_urls=partial(replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user, user=request.user,
) )
...@@ -510,20 +510,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -510,20 +510,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None) ).xmodule_constructor(system)(None, None)
# cdodge: Special case # cdodge: Special case
if module.location.category == 'static_tab': if module.location.category == 'static_tab':
module.get_html = wrap_xmodule( module.get_html = wrap_xmodule(
module.get_html, module.get_html,
module, module,
"xmodule_tab_display.html", "xmodule_tab_display.html",
) )
else: else:
module.get_html = wrap_xmodule( module.get_html = wrap_xmodule(
module.get_html, module.get_html,
module, module,
"xmodule_display.html", "xmodule_display.html",
) )
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
module.get_html, module.get_html,
module.metadata.get('data_dir', module.location.course), module.metadata.get('data_dir', module.location.course),
...@@ -554,7 +554,7 @@ def _xmodule_recurse(item, action): ...@@ -554,7 +554,7 @@ def _xmodule_recurse(item, action):
_xmodule_recurse(child, action) _xmodule_recurse(child, action)
action(item) action(item)
@login_required @login_required
@expect_json @expect_json
...@@ -589,7 +589,7 @@ def delete_item(request): ...@@ -589,7 +589,7 @@ def delete_item(request):
# delete_item on a vertical tries to delete the draft version leaving the # delete_item on a vertical tries to delete the draft version leaving the
# requested delete to never occur # requested delete to never occur
if item.location.revision is None and item.location.category=='vertical' and delete_all_versions: if item.location.revision is None and item.location.category=='vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location) modulestore('direct').delete_item(item.location)
return HttpResponse() return HttpResponse()
...@@ -608,7 +608,7 @@ def save_item(request): ...@@ -608,7 +608,7 @@ def save_item(request):
if request.POST.get('data') is not None: if request.POST.get('data') is not None:
data = request.POST['data'] data = request.POST['data']
store.update_item(item_location, data) store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array # cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually # so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection # deleting the children object from the children collection
...@@ -698,7 +698,7 @@ def unpublish_unit(request): ...@@ -698,7 +698,7 @@ def unpublish_unit(request):
def clone_item(request): def clone_item(request):
parent_location = Location(request.POST['parent_location']) parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template']) template = Location(request.POST['template'])
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location): if not has_access(request.user, parent_location):
...@@ -738,9 +738,9 @@ def upload_asset(request, org, course, coursename): ...@@ -738,9 +738,9 @@ def upload_asset(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename] location = ['i4x', org, course, 'course', coursename]
if not has_access(request.user, location): if not has_access(request.user, location):
return HttpResponseForbidden() return HttpResponseForbidden()
# Does the course actually exist?!? Get anything from it to prove its existance # Does the course actually exist?!? Get anything from it to prove its existance
try: try:
item = modulestore().get_item(location) item = modulestore().get_item(location)
except: except:
...@@ -774,9 +774,9 @@ def upload_asset(request, org, course, coursename): ...@@ -774,9 +774,9 @@ def upload_asset(request, org, course, coursename):
# readback the saved content - we need the database timestamp # readback the saved content - we need the database timestamp
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = {'displayname' : content.name, response_payload = {'displayname' : content.name,
'uploadDate' : get_date_display(readback.last_modified_at), 'uploadDate' : get_date_display(readback.last_modified_at),
'url' : StaticContent.get_url_path_from_location(content.location), 'url' : StaticContent.get_url_path_from_location(content.location),
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg' : 'Upload completed' 'msg' : 'Upload completed'
...@@ -792,7 +792,7 @@ This view will return all CMS users who are editors for the specified course ...@@ -792,7 +792,7 @@ This view will return all CMS users who are editors for the specified course
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def manage_users(request, location): def manage_users(request, location):
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
...@@ -808,7 +808,7 @@ def manage_users(request, location): ...@@ -808,7 +808,7 @@ def manage_users(request, location):
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), 'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id' : request.user.id 'request_user_id' : request.user.id
}) })
def create_json_response(errmsg = None): def create_json_response(errmsg = None):
if errmsg is not None: if errmsg is not None:
...@@ -830,13 +830,13 @@ def add_user(request, location): ...@@ -830,13 +830,13 @@ def add_user(request, location):
if email=='': if email=='':
return create_json_response('Please specify an email address.') return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course # check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
user = get_user_by_email(email) user = get_user_by_email(email)
# user doesn't exist?!? Return error. # user doesn't exist?!? Return error.
if user is None: if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email)) return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
...@@ -859,7 +859,7 @@ the specified course ...@@ -859,7 +859,7 @@ the specified course
@ensure_csrf_cookie @ensure_csrf_cookie
def remove_user(request, location): def remove_user(request, location):
email = request.POST["email"] email = request.POST["email"]
# check that logged in user has admin permissions on this course # check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied() raise PermissionDenied()
...@@ -886,7 +886,7 @@ def landing(request, org, course, coursename): ...@@ -886,7 +886,7 @@ def landing(request, org, course, coursename):
def static_pages(request, org, course, coursename): def static_pages(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename] location = ['i4x', org, course, 'course', coursename]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
...@@ -915,7 +915,7 @@ def reorder_static_tabs(request): ...@@ -915,7 +915,7 @@ def reorder_static_tabs(request):
# get list of existing static tabs in course # get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some! # that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab'] existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs): if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest() return HttpResponseBadRequest()
...@@ -934,15 +934,15 @@ def reorder_static_tabs(request): ...@@ -934,15 +934,15 @@ def reorder_static_tabs(request):
static_tab_idx = 0 static_tab_idx = 0
for tab in course.tabs: for tab in course.tabs:
if tab['type'] == 'static_tab': if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab', reordered_tabs.append({'type': 'static_tab',
'name' : tab_items[static_tab_idx].metadata.get('display_name'), 'name' : tab_items[static_tab_idx].metadata.get('display_name'),
'url_slug' : tab_items[static_tab_idx].location.name}) 'url_slug' : tab_items[static_tab_idx].location.name})
static_tab_idx += 1 static_tab_idx += 1
else: else:
reordered_tabs.append(tab) reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order # OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, course.metadata) modulestore('direct').update_metadata(course.location, course.metadata)
return HttpResponse() return HttpResponse()
...@@ -951,7 +951,7 @@ def reorder_static_tabs(request): ...@@ -951,7 +951,7 @@ def reorder_static_tabs(request):
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def edit_tabs(request, org, course, coursename): def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename] location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location) course_item = modulestore().get_item(location)
static_tabs_loc = Location('i4x', org, course, 'static_tab', None) static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
...@@ -980,7 +980,7 @@ def edit_tabs(request, org, course, coursename): ...@@ -980,7 +980,7 @@ def edit_tabs(request, org, course, coursename):
return render_to_response('edit-tabs.html', { return render_to_response('edit-tabs.html', {
'active_tab': 'pages', 'active_tab': 'pages',
'context_course':course_item, 'context_course':course_item,
'components': components 'components': components
}) })
...@@ -1001,13 +1001,13 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -1001,13 +1001,13 @@ def course_info(request, org, course, name, provided_id=None):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
# get current updates # get current updates
location = ['i4x', org, course, 'course_info', "updates"] location = ['i4x', org, course, 'course_info', "updates"]
...@@ -1018,7 +1018,7 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -1018,7 +1018,7 @@ def course_info(request, org, course, name, provided_id=None):
'course_updates' : json.dumps(get_course_updates(location)), 'course_updates' : json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1032,7 +1032,7 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -1032,7 +1032,7 @@ def course_info_updates(request, org, course, provided_id=None):
# ??? No way to check for access permission afaik # ??? No way to check for access permission afaik
# get current updates # get current updates
location = ['i4x', org, course, 'course_info', "updates"] location = ['i4x', org, course, 'course_info', "updates"]
# Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-( # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
# Possibly due to my removing the seemingly redundant pattern in urls.py # Possibly due to my removing the seemingly redundant pattern in urls.py
if provided_id == '': if provided_id == '':
...@@ -1047,7 +1047,7 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -1047,7 +1047,7 @@ def course_info_updates(request, org, course, provided_id=None):
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else: else:
real_method = request.method real_method = request.method
if request.method == 'GET': if request.method == 'GET':
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json") return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
...@@ -1064,7 +1064,7 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -1064,7 +1064,7 @@ def course_info_updates(request, org, course, provided_id=None):
@ensure_csrf_cookie @ensure_csrf_cookie
def module_info(request, module_location): def module_info(request, module_location):
location = Location(module_location) location = Location(module_location)
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
...@@ -1077,10 +1077,10 @@ def module_info(request, module_location): ...@@ -1077,10 +1077,10 @@ def module_info(request, module_location):
rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true'] rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true']
logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links)) logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links))
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
if real_method == 'GET': if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json") return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
...@@ -1098,20 +1098,20 @@ def get_course_settings(request, org, course, name): ...@@ -1098,20 +1098,20 @@ def get_course_settings(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
course_details = CourseDetails.fetch(location) course_details = CourseDetails.fetch(location)
return render_to_response('settings.html', { return render_to_response('settings.html', {
'active_tab': 'settings', 'active_tab': 'settings',
'context_course': course_module, 'context_course': course_module,
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder) 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1134,13 +1134,13 @@ def course_settings_updates(request, org, course, name, section): ...@@ -1134,13 +1134,13 @@ def course_settings_updates(request, org, course, name, section):
elif section == 'grading': elif section == 'grading':
manager = CourseGradingModel manager = CourseGradingModel
else: return else: return
if request.method == 'GET': if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
@expect_json @expect_json
...@@ -1153,7 +1153,7 @@ def course_grader_updates(request, org, course, name, grader_index=None): ...@@ -1153,7 +1153,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
org, course: Attributes of the Location for the item to edit org, course: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
...@@ -1164,13 +1164,13 @@ def course_grader_updates(request, org, course, name, grader_index=None): ...@@ -1164,13 +1164,13 @@ def course_grader_updates(request, org, course, name, grader_index=None):
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else: else:
real_method = request.method real_method = request.method
if real_method == 'GET': if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)), return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
mimetype="application/json") mimetype="application/json")
elif real_method == "DELETE": elif real_method == "DELETE":
# ??? Shoudl this return anything? Perhaps success fail? # ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index) CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index)
return HttpResponse() return HttpResponse()
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
...@@ -1187,7 +1187,7 @@ def asset_index(request, org, course, name): ...@@ -1187,7 +1187,7 @@ def asset_index(request, org, course, name):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = ['i4x', org, course, 'course', name] location = ['i4x', org, course, 'course', name]
# check that logged in user has permissions to this item # check that logged in user has permissions to this item
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() raise PermissionDenied()
...@@ -1200,7 +1200,7 @@ def asset_index(request, org, course, name): ...@@ -1200,7 +1200,7 @@ def asset_index(request, org, course, name):
}) })
course_module = modulestore().get_item(location) course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name) course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference) assets = contentstore().get_all_content_for_course(course_reference)
...@@ -1214,15 +1214,15 @@ def asset_index(request, org, course, name): ...@@ -1214,15 +1214,15 @@ def asset_index(request, org, course, name):
display_info = {} display_info = {}
display_info['displayname'] = asset['displayname'] display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_date_display(asset['uploadDate']) display_info['uploadDate'] = get_date_display(asset['uploadDate'])
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name']) asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location) display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set # note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None) _thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info) asset_display.append(display_info)
return render_to_response('asset_index.html', { return render_to_response('asset_index.html', {
...@@ -1241,9 +1241,9 @@ def edge(request): ...@@ -1241,9 +1241,9 @@ def edge(request):
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
template = Location(request.POST['template']) template = Location(request.POST['template'])
org = request.POST.get('org') org = request.POST.get('org')
number = request.POST.get('number') number = request.POST.get('number')
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
try: try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
...@@ -1289,13 +1289,13 @@ def initialize_course_tabs(course): ...@@ -1289,13 +1289,13 @@ def initialize_course_tabs(course):
# at least a list populated with the minimal times # at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
course.tabs = [{"type": "courseware"}, course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"}, {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}, {"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}] {"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), course.own_metadata) modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
...@@ -1388,7 +1388,7 @@ def generate_export_course(request, org, course, name): ...@@ -1388,7 +1388,7 @@ def generate_export_course(request, org, course, name):
root_dir = path(mkdtemp()) root_dir = path(mkdtemp())
# export out to a tempdir # export out to a tempdir
logging.debug('root = {0}'.format(root_dir)) logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name) export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
...@@ -1400,7 +1400,7 @@ def generate_export_course(request, org, course, name): ...@@ -1400,7 +1400,7 @@ def generate_export_course(request, org, course, name):
tf.close() tf.close()
# remove temp dir # remove temp dir
shutil.rmtree(root_dir/name) shutil.rmtree(root_dir/name)
wrapper = FileWrapper(export_file) wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz') response = HttpResponse(wrapper, content_type='application/x-tgz')
...@@ -1430,4 +1430,4 @@ def event(request): ...@@ -1430,4 +1430,4 @@ def event(request):
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-) console logs don't get distracted :-)
''' '''
return HttpResponse(True) return HttpResponse(True)
\ No newline at end of file
...@@ -52,10 +52,6 @@ LOGGING = get_logger_config(LOG_DIR, ...@@ -52,10 +52,6 @@ LOGGING = get_logger_config(LOG_DIR,
debug=False, debug=False,
service_variant=SERVICE_VARIANT) service_variant=SERVICE_VARIANT)
with open(ENV_ROOT / "repos.json") as repos_file:
REPOS = json.load(repos_file)
################ SECURE AUTH ITEMS ############################### ################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file: with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
......
...@@ -285,4 +285,5 @@ INSTALLED_APPS = ( ...@@ -285,4 +285,5 @@ INSTALLED_APPS = (
# For asset pipelining # For asset pipelining
'pipeline', 'pipeline',
'staticfiles', 'staticfiles',
'static_replace',
) )
import logging
import re
from staticfiles.storage import staticfiles_storage
from staticfiles import finders
from django.conf import settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.contentstore.content import StaticContent
log = logging.getLogger(__name__)
def try_staticfiles_lookup(path):
"""
Try to lookup a path in staticfiles_storage. If it fails, return
a dead link instead of raising an exception.
"""
try:
url = staticfiles_storage.url(path)
except Exception as err:
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
path, str(err)))
# Just return the original path; don't kill everything.
url = path
return url
def replace(static_url, prefix=None, course_namespace=None):
if prefix is None:
prefix = ''
else:
prefix = prefix + '/'
quote = static_url.group('quote')
servable = (
# If in debug mode, we'll serve up anything that the finders can find
(settings.DEBUG and finders.find(static_url.group('rest'), True)) or
# Otherwise, we'll only serve up stuff that the storages can find
staticfiles_storage.exists(static_url.group('rest'))
)
if servable:
return static_url.group(0)
else:
# don't error if file can't be found
# cdodge: to support the change over to Mongo backed content stores, lets
# use the utility functions in StaticContent.py
if static_url.group('prefix') == '/static/' and not isinstance(modulestore(), XMLModuleStore):
if course_namespace is None:
raise BaseException('You must pass in course_namespace when remapping static content urls with MongoDB stores')
url = StaticContent.convert_legacy_static_url(static_url.group('rest'), course_namespace)
else:
url = try_staticfiles_lookup(prefix + static_url.group('rest'))
new_link = "".join([quote, url, quote])
return new_link
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
def replace_url(static_url):
return replace(static_url, staticfiles_prefix, course_namespace = course_namespace)
return re.sub(r"""
(?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # the prefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=replace_prefix), replace_url, text)
import logging
import re
from staticfiles.storage import staticfiles_storage
from staticfiles import finders
from django.conf import settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.contentstore.content import StaticContent
log = logging.getLogger(__name__)
def _url_replace_regex(prefix):
return r"""
(?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # theeprefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=prefix)
def try_staticfiles_lookup(path):
"""
Try to lookup a path in staticfiles_storage. If it fails, return
a dead link instead of raising an exception.
"""
try:
url = staticfiles_storage.url(path)
except Exception as err:
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
path, str(err)))
# Just return the original path; don't kill everything.
url = path
return url
def replace_course_urls(text, course_id):
"""
Replace /course/$stuff urls with /courses/$course_id/$stuff urls
text: The text to replace
course_module: A CourseDescriptor
returns: text with the links replaced
"""
def replace_course_url(match):
quote = match.group('quote')
rest = match.group('rest')
return "".join([quote, '/courses/' + course_id + '/', rest, quote])
return re.sub(_url_replace_regex('/course/'), replace_course_url, text)
def replace_static_urls(text, data_directory, course_namespace=None):
"""
Replace /static/$stuff urls either with their correct url as generated by collectstatic,
(/static/$md5_hashed_stuff) or by the course-specific content static url
/static/$course_data_dir/$stuff, or, if course_namespace is not None, by the
correct url in the contentstore (c4x://)
text: The source text to do the substitution in
data_directory: The directory in which course data is stored
course_namespace: The course identifier used to distinguish static content for this course in studio
"""
def replace_static_url(match):
original = match.group(0)
prefix = match.group('prefix')
quote = match.group('quote')
rest = match.group('rest')
# course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# If we're in debug mode, and the file as requested exists, then don't change the links
elif (settings.DEBUG and finders.find(rest, True)):
return original
# Otherwise, look the file up in staticfiles_storage without the data directory
else:
try:
url = staticfiles_storage.url(rest)
# And if that fails, assume that it's course content, and add manually data directory
except Exception as err:
log.warning("staticfiles_storage couldn't find path {0}: {1}".format(
rest, str(err)))
url = "".join([prefix, data_directory, '/', rest])
return "".join([quote, url, quote])
return re.sub(
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)),
replace_static_url,
text
)
###
### Script for importing courseware from XML format
###
from django.core.management.base import NoArgsCommand
from django.core.cache import get_cache
class Command(NoArgsCommand):
help = \
'''Import the specified data directory into the default ModuleStore'''
def handle_noargs(self, **options):
staticfiles_cache = get_cache('staticfiles')
staticfiles_cache.clear()
from nose.tools import assert_equals
from static_replace import replace_static_urls, replace_course_urls
from mock import patch, Mock
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml import XMLModuleStore
DATA_DIRECTORY = 'data_dir'
COURSE_ID = 'org/course/run'
NAMESPACE = Location('org', 'course', 'run', None, None)
STATIC_SOURCE = '"/static/file.png"'
def test_multi_replace():
course_source = '"/course/file.png"'
assert_equals(
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY),
replace_static_urls(replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY), DATA_DIRECTORY)
)
assert_equals(
replace_course_urls(course_source, COURSE_ID),
replace_course_urls(replace_course_urls(course_source, COURSE_ID), COURSE_ID)
)
@patch('static_replace.finders')
@patch('static_replace.settings')
def test_debug_no_modify(mock_settings, mock_finders):
mock_settings.DEBUG = True
mock_finders.find.return_value = True
assert_equals(STATIC_SOURCE, replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
mock_finders.find.assert_called_once_with('file.png', True)
@patch('static_replace.StaticContent')
@patch('static_replace.modulestore')
def test_mongo_filestore(mock_modulestore, mock_static_content):
mock_modulestore.return_value = Mock(MongoModuleStore)
mock_static_content.convert_legacy_static_url.return_value = "c4x://mock_url"
# No namespace => no change to path
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
# Namespace => content url
assert_equals(
'"' + mock_static_content.convert_legacy_static_url.return_value + '"',
replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY, NAMESPACE)
)
mock_static_content.convert_legacy_static_url.assert_called_once_with('file.png', NAMESPACE)
@patch('static_replace.settings')
@patch('static_replace.modulestore')
@patch('static_replace.staticfiles_storage')
def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
mock_modulestore.return_value = Mock(XMLModuleStore)
mock_settings.DEBUG = False
mock_storage.url.side_effect = Exception
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
...@@ -2,10 +2,10 @@ import re ...@@ -2,10 +2,10 @@ import re
import json import json
import logging import logging
import time import time
import static_replace
from django.conf import settings from django.conf import settings
from functools import wraps from functools import wraps
from static_replace import replace_urls
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from xmodule.seq_module import SequenceModule from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule from xmodule.vertical_module import VerticalModule
...@@ -49,10 +49,10 @@ def replace_course_urls(get_html, course_id): ...@@ -49,10 +49,10 @@ def replace_course_urls(get_html, course_id):
""" """
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') return static_replace.replace_course_urls(get_html(), course_id)
return _get_html return _get_html
def replace_static_urls(get_html, prefix, course_namespace=None): def replace_static_urls(get_html, data_dir, course_namespace=None):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/... the old get_html function and substitutes urls of the form /static/...
...@@ -61,10 +61,9 @@ def replace_static_urls(get_html, prefix, course_namespace=None): ...@@ -61,10 +61,9 @@ def replace_static_urls(get_html, prefix, course_namespace=None):
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
return replace_urls(get_html(), staticfiles_prefix=prefix, course_namespace = course_namespace) return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
return _get_html return _get_html
def grade_histogram(module_id): def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem. ''' Print out a histogram of grades on a given problem.
Part of staff member debug info. Part of staff member debug info.
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var MinimaxProblemDisplay, root, var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var TestProblemGenerator, root, var TestProblemGenerator, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var TestProblemGrader, root, var TestProblemGrader, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var XProblemDisplay, XProblemGenerator, XProblemGrader, root; var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
......
...@@ -27,6 +27,7 @@ setup( ...@@ -27,6 +27,7 @@ setup(
"html = xmodule.html_module:HtmlDescriptor", "html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"problem = xmodule.capa_module:CapaDescriptor", "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor", "randomize = xmodule.randomize_module:RandomizeDescriptor",
......
...@@ -369,7 +369,7 @@ class CapaModule(XModule): ...@@ -369,7 +369,7 @@ class CapaModule(XModule):
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>" id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes # now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location) return self.system.replace_urls(html)
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
...@@ -490,7 +490,7 @@ class CapaModule(XModule): ...@@ -490,7 +490,7 @@ class CapaModule(XModule):
new_answers = dict() new_answers = dict()
for answer_id in answers: for answer_id in answers:
try: try:
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)} new_answer = {answer_id: self.system.replace_urls(answers[answer_id])}
except TypeError: except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id])) log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]} new_answer = {answer_id: answers[answer_id]}
......
...@@ -36,7 +36,9 @@ class CombinedOpenEndedRubric(object): ...@@ -36,7 +36,9 @@ class CombinedOpenEndedRubric(object):
'max_score': max_score}) 'max_score': max_score})
success = True success = True
except: except:
raise RubricParsingError("[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)) error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)
log.error(error_message)
raise RubricParsingError(error_message)
return success, html return success, html
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score):
......
...@@ -52,13 +52,17 @@ em, i { ...@@ -52,13 +52,17 @@ em, i {
} }
strong, b { strong, b {
font-style: bold; font-weight: bold;
} }
p + p, ul + p, ol + p { p + p, ul + p, ol + p {
margin-top: 20px; margin-top: 20px;
} }
blockquote {
margin: 1em 40px;
}
ol, ul { ol, ul {
margin: 1em 0; margin: 1em 0;
padding: 0 0 0 1em; padding: 0 0 0 1em;
......
...@@ -5,16 +5,8 @@ import requests ...@@ -5,16 +5,8 @@ import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from django.conf import settings
from django.http import HttpResponse, Http404
from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from lxml import etree from lxml import etree
from mitxmako.shortcuts import render_to_string
from xmodule.x_module import ModuleSystem
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -31,7 +23,7 @@ class GradingService(object): ...@@ -31,7 +23,7 @@ class GradingService(object):
self.url = config['url'] self.url = config['url']
self.login_url = self.url + '/login/' self.login_url = self.url + '/login/'
self.session = requests.session() self.session = requests.session()
self.system = ModuleSystem(None, None, None, render_to_string, None) self.system = config['system']
def _login(self): def _login(self):
""" """
...@@ -42,20 +34,20 @@ class GradingService(object): ...@@ -42,20 +34,20 @@ class GradingService(object):
Returns the decoded json dict of the response. Returns the decoded json dict of the response.
""" """
response = self.session.post(self.login_url, response = self.session.post(self.login_url,
{'username': self.username, {'username': self.username,
'password': self.password,}) 'password': self.password,})
response.raise_for_status() response.raise_for_status()
return response.json return response.json
def post(self, url, data, allow_redirects=False): def post(self, url, data, allow_redirects=False):
""" """
Make a post request to the grading controller Make a post request to the grading controller
""" """
try: try:
op = lambda: self.session.post(url, data=data, op = lambda: self.session.post(url, data=data,
allow_redirects=allow_redirects) allow_redirects=allow_redirects)
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace. # reraise as promised GradingServiceError, but preserve stacktrace.
...@@ -69,8 +61,8 @@ class GradingService(object): ...@@ -69,8 +61,8 @@ class GradingService(object):
""" """
log.debug(params) log.debug(params)
op = lambda: self.session.get(url, op = lambda: self.session.get(url,
allow_redirects=allow_redirects, allow_redirects=allow_redirects,
params=params) params=params)
try: try:
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
...@@ -78,7 +70,7 @@ class GradingService(object): ...@@ -78,7 +70,7 @@ class GradingService(object):
raise GradingServiceError, str(err), sys.exc_info()[2] raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text return r.text
def _try_with_login(self, operation): def _try_with_login(self, operation):
""" """
...@@ -96,8 +88,8 @@ class GradingService(object): ...@@ -96,8 +88,8 @@ class GradingService(object):
r = self._login() r = self._login()
if r and not r.get('success'): if r and not r.get('success'):
log.warning("Couldn't log into staff_grading backend. Response: %s", log.warning("Couldn't log into staff_grading backend. Response: %s",
r) r)
# try again # try again
response = operation() response = operation()
response.raise_for_status() response.raise_for_status()
...@@ -113,23 +105,23 @@ class GradingService(object): ...@@ -113,23 +105,23 @@ class GradingService(object):
""" """
try: try:
response_json = json.loads(response) response_json = json.loads(response)
except:
response_json = response
try:
if 'rubric' in response_json: if 'rubric' in response_json:
rubric = response_json['rubric'] rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(self.system, view_only) rubric_renderer = CombinedOpenEndedRubric(self.system, view_only)
success, rubric_html = rubric_renderer.render_rubric(rubric) success, rubric_html = rubric_renderer.render_rubric(rubric)
response_json['rubric'] = rubric_html response_json['rubric'] = rubric_html
return response_json return response_json
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError, RubricParsingError: except etree.XMLSyntaxError, RubricParsingError:
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric)) .format(rubric))
return {'success': False, return {'success': False,
'error': 'Error displaying submission'} 'error': 'Error displaying submission'}
except ValueError: except ValueError:
log.exception("Error parsing response: {0}".format(response)) log.exception("Error parsing response: {0}".format(response))
return {'success': False, return {'success': False,
'error': "Error displaying submission"} 'error': "Error displaying submission"}
...@@ -10,7 +10,8 @@ class @HTMLEditingDescriptor ...@@ -10,7 +10,8 @@ class @HTMLEditingDescriptor
lineWrapping: true lineWrapping: true
}) })
$(@advanced_editor.getWrapperElement()).addClass(HTMLEditingDescriptor.isInactiveClass) @$advancedEditorWrapper = $(@advanced_editor.getWrapperElement())
@$advancedEditorWrapper.addClass(HTMLEditingDescriptor.isInactiveClass)
# This is a workaround for the fact that tinyMCE's baseURL property is not getting correctly set on AWS # This is a workaround for the fact that tinyMCE's baseURL property is not getting correctly set on AWS
# instances (like sandbox). It is not necessary to explicitly set baseURL when running locally. # instances (like sandbox). It is not necessary to explicitly set baseURL when running locally.
...@@ -43,16 +44,21 @@ class @HTMLEditingDescriptor ...@@ -43,16 +44,21 @@ class @HTMLEditingDescriptor
theme_advanced_blockformats : "p,pre,h1,h2,h3", theme_advanced_blockformats : "p,pre,h1,h2,h3",
width: '100%', width: '100%',
height: '400px', height: '400px',
setup : HTMLEditingDescriptor.setupTinyMCE, setup : @setupTinyMCE,
# Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered. # Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered.
# The tinyMCE callback passes in the editor as a paramter. # The tinyMCE callback passes in the editor as a paramter.
init_instance_callback: @focusVisualEditor init_instance_callback: @focusVisualEditor
}) })
@showingVisualEditor = true @showingVisualEditor = true
@element.on('click', '.editor-tabs .tab', this, @onSwitchEditor) # Doing these find operations within onSwitchEditor leads to sporadic failures on Chrome (version 20 and older).
$element = $(element)
@$htmlTab = $element.find('.html-tab')
@$visualTab = $element.find('.visual-tab')
@setupTinyMCE: (ed) -> @element.on('click', '.editor-tabs .tab', @onSwitchEditor)
setupTinyMCE: (ed) =>
ed.addButton('wrapAsCode', { ed.addButton('wrapAsCode', {
title : 'Code', title : 'Code',
image : '/static/images/ico-tinymce-code.png', image : '/static/images/ico-tinymce-code.png',
...@@ -67,22 +73,23 @@ class @HTMLEditingDescriptor ...@@ -67,22 +73,23 @@ class @HTMLEditingDescriptor
command.setActive('wrapAsCode', e.nodeName == 'CODE') command.setActive('wrapAsCode', e.nodeName == 'CODE')
) )
onSwitchEditor: (e)=> @visualEditor = ed
e.preventDefault();
if not $(e.currentTarget).hasClass('current') onSwitchEditor: (e) =>
element = e.data.element e.preventDefault();
$(e.currentTarget).addClass('current') $currentTarget = $(e.currentTarget)
$(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass) if not $currentTarget.hasClass('current')
$(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass) $currentTarget.addClass('current')
@$mceToolbar.toggleClass(HTMLEditingDescriptor.isInactiveClass)
@$advancedEditorWrapper.toggleClass(HTMLEditingDescriptor.isInactiveClass)
visualEditor = @getVisualEditor(element) visualEditor = @getVisualEditor()
if $(e.currentTarget).attr('data-tab') is 'visual' if $currentTarget.data('tab') is 'visual'
$(element).find('.html-tab').removeClass('current') @$htmlTab.removeClass('current')
@showVisualEditor(visualEditor) @showVisualEditor(visualEditor)
else else
$(element).find('.visual-tab').removeClass('current') @$visualTab.removeClass('current')
@showAdvancedEditor(visualEditor) @showAdvancedEditor(visualEditor)
# Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing. # Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
...@@ -104,20 +111,24 @@ class @HTMLEditingDescriptor ...@@ -104,20 +111,24 @@ class @HTMLEditingDescriptor
@focusVisualEditor(visualEditor) @focusVisualEditor(visualEditor)
@showingVisualEditor = true @showingVisualEditor = true
focusVisualEditor: (visualEditor) -> focusVisualEditor: (visualEditor) =>
visualEditor.focus() visualEditor.focus()
if not @$mceToolbar?
@$mceToolbar = $(@element).find('table.mceToolbar')
getVisualEditor: (element) -> getVisualEditor: () ->
### ###
Returns the instance of TinyMCE. Returns the instance of TinyMCE.
This is different from the textarea that exists in the HTML template (@tiny_mce_textarea. This is different from the textarea that exists in the HTML template (@tiny_mce_textarea.
Pulled out as a helper method for unit test.
### ###
return tinyMCE.get($(element).find('.tiny-mce').attr('id')) return @visualEditor
save: -> save: ->
@element.off('click', '.editor-tabs .tab', @onSwitchEditor) @element.off('click', '.editor-tabs .tab', @onSwitchEditor)
text = @advanced_editor.getValue() text = @advanced_editor.getValue()
visualEditor = @getVisualEditor(@element) visualEditor = @getVisualEditor()
if @showingVisualEditor and visualEditor.isDirty() if @showingVisualEditor and visualEditor.isDirty()
text = visualEditor.getContent({no_events: 1}) text = visualEditor.getContent({no_events: 1})
data: text data: text
...@@ -2,17 +2,27 @@ ...@@ -2,17 +2,27 @@
# and message container when they are empty # and message container when they are empty
# Can (and should be) expanded upon when our problem list # Can (and should be) expanded upon when our problem list
# becomes more sophisticated # becomes more sophisticated
class PeerGrading class @PeerGrading
constructor: () -> constructor: (element) ->
@peer_grading_container = $('.peer-grading')
@use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container')
@ajax_url = @peer_grading_container.data('ajax-url')
@error_container = $('.error-container') @error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty')) @error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container') @message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty')) @message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@problem_list = $('.problem-list') @problem_list = $('.problem-list')
@construct_progress_bar() @construct_progress_bar()
if @use_single_location
@activate_problem()
construct_progress_bar: () => construct_progress_bar: () =>
problems = @problem_list.find('tr').next() problems = @problem_list.find('tr').next()
problems.each( (index, element) => problems.each( (index, element) =>
...@@ -22,6 +32,18 @@ class PeerGrading ...@@ -22,6 +32,18 @@ class PeerGrading
bar_max = parseInt(problem.data('required')) + bar_value bar_max = parseInt(problem.data('required')) + bar_value
progress_bar.progressbar({value: bar_value, max: bar_max}) progress_bar.progressbar({value: bar_value, max: bar_max})
) )
$(document).ready(() -> new PeerGrading()) show_results: (event) =>
location_to_fetch = $(event.target).data('location')
data = {'location' : location_to_fetch}
$.postWithPrefix "#{@ajax_url}problem", data, (response) =>
if response.success
@peer_grading_outer_container.after(response.html).remove()
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
else
@gentle_alert response.error
activate_problem: () =>
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
\ No newline at end of file
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
# Should not be run when we don't have a location to send back # Should not be run when we don't have a location to send back
# to the server # to the server
# #
# PeerGradingProblemBackend - # PeerGradingProblemBackend -
# makes all the ajax requests and provides a mock interface # makes all the ajax requests and provides a mock interface
# for testing purposes # for testing purposes
# #
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
# handles the rendering and user interactions with the interface # handles the rendering and user interactions with the interface
# #
################################## ##################################
class PeerGradingProblemBackend class @PeerGradingProblemBackend
constructor: (ajax_url, mock_backend) -> constructor: (ajax_url, mock_backend) ->
@mock_backend = mock_backend @mock_backend = mock_backend
@ajax_url = ajax_url @ajax_url = ajax_url
...@@ -32,141 +32,140 @@ class PeerGradingProblemBackend ...@@ -32,141 +32,140 @@ class PeerGradingProblemBackend
mock: (cmd, data) -> mock: (cmd, data) ->
if cmd == 'is_student_calibrated' if cmd == 'is_student_calibrated'
# change to test each version # change to test each version
response = response =
success: true success: true
calibrated: @mock_cnt >= 2 calibrated: @mock_cnt >= 2
else if cmd == 'show_calibration_essay' else if cmd == 'show_calibration_essay'
#response = #response =
# success: false # success: false
# error: "There was an error" # error: "There was an error"
@mock_cnt++ @mock_cnt++
response = response =
success: true success: true
submission_id: 1 submission_id: 1
submission_key: 'abcd' submission_key: 'abcd'
student_response: ''' student_response: '''
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
''' '''
prompt: ''' prompt: '''
<h2>S11E3: Metal Bands</h2> <h2>S11E3: Metal Bands</h2>
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p> <p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p> <p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p> <p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
''' '''
rubric: ''' rubric: '''
<table class="rubric"><tbody><tr><th>Purpose</th> <table class="rubric"><tbody><tr><th>Purpose</th>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
</td> </td>
</tr><tr><th>Organization</th> </tr><tr><th>Organization</th>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
</td> </td>
</tr></tbody></table> </tr></tbody></table>
''' '''
max_score: 4 max_score: 4
else if cmd == 'get_next_submission' else if cmd == 'get_next_submission'
response = response =
success: true success: true
submission_id: 1 submission_id: 1
submission_key: 'abcd' submission_key: 'abcd'
student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa. student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa.
Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum. Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum.
Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. ''' Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. '''
prompt: ''' prompt: '''
<h2>S11E3: Metal Bands</h2> <h2>S11E3: Metal Bands</h2>
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p> <p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p> <p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p> <p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
''' '''
rubric: ''' rubric: '''
<table class="rubric"><tbody><tr><th>Purpose</th> <table class="rubric"><tbody><tr><th>Purpose</th>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
</td> </td>
</tr><tr><th>Organization</th> </tr><tr><th>Organization</th>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
</td> </td>
<td> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label> <input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
</td> </td>
</tr></tbody></table> </tr></tbody></table>
''' '''
max_score: 4 max_score: 4
else if cmd == 'save_calibration_essay' else if cmd == 'save_calibration_essay'
response = response =
success: true success: true
actual_score: 2 actual_score: 2
else if cmd == 'save_grade' else if cmd == 'save_grade'
response = response =
success: true success: true
return response return response
class @PeerGradingProblem
class PeerGradingProblem
constructor: (backend) -> constructor: (backend) ->
@prompt_wrapper = $('.prompt-wrapper') @prompt_wrapper = $('.prompt-wrapper')
@backend = backend @backend = backend
# get the location of the problem # get the location of the problem
@location = $('.peer-grading').data('location') @location = $('.peer-grading').data('location')
# prevent this code from trying to run # prevent this code from trying to run
# when we don't have a location # when we don't have a location
if(!@location) if(!@location)
return return
...@@ -208,7 +207,7 @@ class PeerGradingProblem ...@@ -208,7 +207,7 @@ class PeerGradingProblem
# Set up the click event handlers # Set up the click event handlers
@action_button.click -> history.back() @action_button.click -> history.back()
@calibration_feedback_button.click => @calibration_feedback_button.click =>
@calibration_feedback_panel.hide() @calibration_feedback_panel.hide()
@grading_wrapper.show() @grading_wrapper.show()
@is_calibrated_check() @is_calibrated_check()
...@@ -254,7 +253,7 @@ class PeerGradingProblem ...@@ -254,7 +253,7 @@ class PeerGradingProblem
submit_grade: () => submit_grade: () =>
data = @construct_data() data = @construct_data()
@backend.post('save_grade', data, @submission_callback) @backend.post('save_grade', data, @submission_callback)
########## ##########
# #
...@@ -289,7 +288,7 @@ class PeerGradingProblem ...@@ -289,7 +288,7 @@ class PeerGradingProblem
@render_calibration_feedback(response) @render_calibration_feedback(response)
else if response.error else if response.error
@render_error(response.error) @render_error(response.error)
else else
@render_error("Error saving calibration score") @render_error("Error saving calibration score")
# called after we submit a submission score # called after we submit a submission score
...@@ -311,8 +310,8 @@ class PeerGradingProblem ...@@ -311,8 +310,8 @@ class PeerGradingProblem
# show button if we have scores for all categories # show button if we have scores for all categories
@show_submit_button() @show_submit_button()
########## ##########
# #
# Rendering methods and helpers # Rendering methods and helpers
...@@ -325,7 +324,7 @@ class PeerGradingProblem ...@@ -325,7 +324,7 @@ class PeerGradingProblem
# load in all the data # load in all the data
@submission_container.html("<h3>Training Essay</h3>") @submission_container.html("<h3>Training Essay</h3>")
@render_submission_data(response) @render_submission_data(response)
# TODO: indicate that we're in calibration mode # TODO: indicate that we're in calibration mode
@calibration_panel.addClass('current-state') @calibration_panel.addClass('current-state')
@grading_panel.removeClass('current-state') @grading_panel.removeClass('current-state')
...@@ -409,12 +408,12 @@ class PeerGradingProblem ...@@ -409,12 +408,12 @@ class PeerGradingProblem
if score == actual_score if score == actual_score
calibration_wrapper.append("<p>Congratulations! Your score matches the actual score!</p>") calibration_wrapper.append("<p>Congratulations! Your score matches the actual score!</p>")
else else
calibration_wrapper.append("<p>Please try to understand the grading critera better to be more accurate next time.</p>") calibration_wrapper.append("<p>Please try to understand the grading critera better to be more accurate next time.</p>")
# disable score selection and submission from the grading interface # disable score selection and submission from the grading interface
$("input[name='score-selection']").attr('disabled', true) $("input[name='score-selection']").attr('disabled', true)
@submit_button.hide() @submit_button.hide()
render_interstitial_page: () => render_interstitial_page: () =>
@content_panel.hide() @content_panel.hide()
@interstitial_page.show() @interstitial_page.show()
...@@ -432,10 +431,3 @@ class PeerGradingProblem ...@@ -432,10 +431,3 @@ class PeerGradingProblem
setup_score_selection: (max_score) => setup_score_selection: (max_score) =>
# And now hook up an event handler again # And now hook up an event handler again
$("input[class='score-selection']").change @graded_callback $("input[class='score-selection']").change @graded_callback
mock_backend = false
ajax_url = $('.peer-grading').data('ajax_url')
backend = new PeerGradingProblemBackend(ajax_url, mock_backend)
$(document).ready(() -> new PeerGradingProblem(backend))
...@@ -13,6 +13,10 @@ from urlparse import urlparse ...@@ -13,6 +13,10 @@ from urlparse import urlparse
import requests import requests
from boto.s3.connection import S3Connection from boto.s3.connection import S3Connection
from boto.s3.key import Key from boto.s3.key import Key
#TODO: Settings import is needed now in order to specify the URL and keys for amazon s3 (to upload images).
#Eventually, the goal is to replace the global django settings import with settings specifically
#for this module. There is no easy way to do this now, so piggybacking on the django settings
#makes sense.
from django.conf import settings from django.conf import settings
import pickle import pickle
import logging import logging
......
"""
This module provides an interface on the grading-service backend
for peer grading
Use peer_grading_service() to get the version specified
in settings.PEER_GRADING_INTERFACE
"""
import json
import logging
import requests
import sys
from django.conf import settings
from combined_open_ended_rubric import CombinedOpenEndedRubric
from lxml import etree
import copy
import itertools
import json
import logging
from lxml.html import rewrite_links
import os
from pkg_resources import resource_string
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
from peer_grading_service import peer_grading_service, GradingServiceError
log = logging.getLogger(__name__)
USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = ""
TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1
IS_GRADED = True
class PeerGradingModule(XModule):
_VERSION = 1
js = {'coffee': [resource_string(__name__, 'js/src/peergrading/peer_grading.coffee'),
resource_string(__name__, 'js/src/peergrading/peer_grading_problem.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "PeerGrading"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
#We need to set the location here so the child modules can use it
system.set('location', location)
self.system = system
self.peer_gs = peer_grading_service(self.system)
self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
if isinstance(self.use_for_single_location, basestring):
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT)
self.is_graded = self.metadata.get('is_graded', IS_GRADED)
if isinstance(self.is_graded, basestring):
self.is_graded = (self.is_graded in TRUE_DICT)
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
if self.use_for_single_location ==True:
#This will raise an exception if the location is invalid
link_to_location_object = Location(self.link_to_location)
self.ajax_url = self.system.ajax_url
if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/"
self.student_data_for_location = instance_state.get('student_data_for_location', {})
self.max_grade = instance_state.get('max_grade', MAX_SCORE)
if not isinstance(self.max_grade, (int, long)):
#This could result in an exception, but not wrapping in a try catch block so it moves up the stack
self.max_grade = int(self.max_grade)
def _err_response(self, msg):
"""
Return a HttpResponse with a json dump with success=False, and the given error message.
"""
return {'success': False, 'error': msg}
def _check_required(self, get, required):
actual = set(get.keys())
missing = required - actual
if len(missing) > 0:
return False, "Missing required keys: {0}".format(', '.join(missing))
else:
return True, ""
def get_html(self):
"""
Needs to be implemented by inheritors. Renders the HTML that students see.
@return:
"""
if not self.use_for_single_location:
return self.peer_grading()
else:
return self.peer_grading_problem({'location' : self.link_to_location})['html']
def handle_ajax(self, dispatch, get):
"""
Needs to be implemented by child modules. Handles AJAX events.
@return:
"""
handlers = {
'get_next_submission': self.get_next_submission,
'show_calibration_essay': self.show_calibration_essay,
'is_student_calibrated': self.is_student_calibrated,
'save_grade': self.save_grade,
'save_calibration_essay' : self.save_calibration_essay,
'problem' : self.peer_grading_problem,
}
if dispatch not in handlers:
return 'Error'
d = handlers[dispatch](get)
return json.dumps(d, cls=ComplexEncoder)
def query_data_for_location(self):
student_id = self.system.anonymous_student_id
location = self.system.location
success = False
response = {}
try:
response = self.peer_gs.get_data_for_location(location, student_id)
count_graded = response['count_graded']
count_required = response['count_required']
success = True
except GradingServiceError:
log.exception("Error getting location data from controller for location {0}, student {1}"
.format(location, student_id))
return success, response
def get_progress(self):
pass
def get_score(self):
if not self.use_for_single_location or not self.is_graded:
return None
try:
count_graded = self.student_data_for_location['count_graded']
count_required = self.student_data_for_location['count_required']
except:
success, response = self.query_data_for_location()
if not success:
log.exception("No instance data found and could not get data from controller for loc {0} student {1}".format(
self.system.location, self.system.anonymous_student_id
))
return None
count_graded = response['count_graded']
count_required = response['count_required']
if count_required>0 and count_graded>=count_required:
self.student_data_for_location = response
score_dict = {
'score': int(count_graded>=count_required),
'total': self.max_grade,
}
return score_dict
def max_score(self):
''' Maximum score. Two notes:
* This is generic; in abstract, a problem could be 3/5 points on one
randomization, and 5/7 on another
'''
max_grade = None
if self.use_for_single_location and self.is_graded:
max_grade = self.max_grade
return max_grade
def get_next_submission(self, get):
"""
Makes a call to the grading controller for the next essay that should be graded
Returns a json dict with the following keys:
'success': bool
'submission_id': a unique identifier for the submission, to be passed back
with the grade.
'submission': the submission, rendered as read-only html for grading
'rubric': the rubric, also rendered as html.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
required = set(['location'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
grader_id = self.system.anonymous_student_id
location = get['location']
try:
response = self.peer_gs.get_next_submission(location, grader_id)
return response
except GradingServiceError:
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(self.peer_gs.url, location, grader_id))
return {'success': False,
'error': 'Could not connect to grading service'}
def save_grade(self, get):
"""
Saves the grade of a given submission.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
"""
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
grader_id = self.system.anonymous_student_id
location = get.get('location')
submission_id = get.get('submission_id')
score = get.get('score')
feedback = get.get('feedback')
submission_key = get.get('submission_key')
rubric_scores = get.getlist('rubric_scores[]')
submission_flagged = get.get('submission_flagged')
try:
response = self.peer_gs.save_grade(location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged)
return response
except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}"""
.format(self.peer_gs.url,
location, submission_id, submission_key, score)
)
return {
'success': False,
'error': 'Could not connect to grading service'
}
def is_student_calibrated(self, get):
"""
Calls the grading controller to see if the given student is calibrated
on the given problem
Input:
In the request, we need the following arguments:
location - problem location
Returns:
Json object with the following keys
success - bool indicating whether or not the call was successful
calibrated - true if the grader has fully calibrated and can now move on to grading
- false if the grader is still working on calibration problems
total_calibrated_on_so_far - the number of calibration essays for this problem
that this grader has graded
"""
required = set(['location'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
grader_id = self.system.anonymous_student_id
location = get['location']
try:
response = self.peer_gs.is_student_calibrated(location, grader_id)
return response
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(self.peer_gs.url, grader_id, location))
return {
'success': False,
'error': 'Could not connect to grading service'
}
def show_calibration_essay(self, get):
"""
Fetch the next calibration essay from the grading controller and return it
Inputs:
In the request
location - problem location
Returns:
A json dict with the following keys
'success': bool
'submission_id': a unique identifier for the submission, to be passed back
with the grade.
'submission': the submission, rendered as read-only html for grading
'rubric': the rubric, also rendered as html.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
required = set(['location'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
grader_id = self.system.anonymous_student_id
location = get['location']
try:
response = self.peer_gs.show_calibration_essay(location, grader_id)
return response
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, location: {0}"
.format(self.peer_gs.url, location))
return {'success': False,
'error': 'Could not connect to grading service'}
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError:
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
return {'success': False,
'error': 'Error displaying submission'}
def save_calibration_essay(self, get):
"""
Saves the grader's grade of a given calibration.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
actual_score: the score that the instructor gave to this calibration essay
"""
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
success, message = self._check_required(get, required)
if not success:
return self._err_response(message)
grader_id = self.system.anonymous_student_id
location = get.get('location')
calibration_essay_id = get.get('submission_id')
submission_key = get.get('submission_key')
score = get.get('score')
feedback = get.get('feedback')
rubric_scores = get.getlist('rubric_scores[]')
try:
response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores)
return response
except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
return self._err_response('Could not connect to grading service')
def peer_grading(self, get = None):
'''
Show a peer grading interface
'''
# call problem list service
success = False
error_text = ""
problem_list = []
try:
problem_list_json = self.peer_gs.get_problem_list(self.system.course_id, self.system.anonymous_student_id)
problem_list_dict = problem_list_json
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = problem_list_dict['problem_list']
except GradingServiceError:
error_text = "Error occured while contacting the grading service"
success = False
# catch error if if the json loads fails
except ValueError:
error_text = "Could not get problem list"
success = False
ajax_url = self.ajax_url
html = self.system.render_template('peer_grading/peer_grading.html', {
'course_id': self.system.course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': False,
'use_single_location' : self.use_for_single_location,
})
return html
def peer_grading_problem(self, get = None):
'''
Show individual problem interface
'''
if get == None or get.get('location')==None:
if not self.use_for_single_location:
#This is an error case, because it must be set to use a single location to be called without get parameters
return {'html' : "", 'success' : False}
problem_location = self.link_to_location
elif get.get('location') is not None:
problem_location = get.get('location')
ajax_url = self.ajax_url
html = self.system.render_template('peer_grading/peer_grading_problem.html', {
'view_html': '',
'problem_location': problem_location,
'course_id': self.system.course_id,
'ajax_url': ajax_url,
# Checked above
'staff_access': False,
'use_single_location' : self.use_for_single_location,
})
return {'html' : html, 'success' : True}
def get_instance_state(self):
"""
Returns the current instance state. The module can be recreated from the instance state.
Input: None
Output: A dictionary containing the instance state.
"""
state = {
'student_data_for_location' : self.student_data_for_location,
}
return json.dumps(state)
class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding combined open ended questions
"""
mako_template = "widgets/html-edit.html"
module_class = PeerGradingModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "peer_grading"
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,
}
"""
log.debug("In definition")
expected_children = []
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
raise ValueError("Peer grading definition must include at least one '{0}' tag".format(child))
def parse_task(k):
"""Assumes that xml_object has child k"""
return [stringify_children(xml_object.xpath(k)[i]) for i in xrange(0, len(xml_object.xpath(k)))]
def parse(k):
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
return {}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('peergrading')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt
\ No newline at end of file
import json
import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
#TODO: Settings import is needed now in order to specify the URL where to find the peer grading service.
#Eventually, the goal is to replace the global django settings import with settings specifically
#for this xmodule. There is no easy way to do this now, so piggybacking on the django settings
#makes sense.
from django.conf import settings
from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from lxml import etree
from grading_service_module import GradingService, GradingServiceError
log=logging.getLogger(__name__)
class GradingServiceError(Exception):
pass
class PeerGradingService(GradingService):
"""
Interface with the grading controller for peer grading
"""
def __init__(self, config, system):
config['system'] = system
super(PeerGradingService, self).__init__(config)
self.get_next_submission_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/'
self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
self.get_problem_list_url = self.url + '/get_problem_list/'
self.get_notifications_url = self.url + '/get_notifications/'
self.get_data_for_location_url = self.url + '/get_data_for_location/'
self.system = system
def get_data_for_location(self, problem_location, student_id):
response = self.get(self.get_data_for_location_url,
{'location': problem_location, 'student_id': student_id})
return self.try_to_decode(response)
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
data = {'grader_id' : grader_id,
'submission_id' : submission_id,
'score' : score,
'feedback' : feedback,
'submission_key': submission_key,
'location': location,
'rubric_scores': rubric_scores,
'rubric_scores_complete': True,
'submission_flagged' : submission_flagged}
return self.try_to_decode(self.post(self.save_grade_url, data))
def is_student_calibrated(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.try_to_decode(self.get(self.is_student_calibrated_url, params))
def show_calibration_essay(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
response = self.get(self.show_calibration_essay_url, params)
return self.try_to_decode(self._render_rubric(response))
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key,
score, feedback, rubric_scores):
data = {'location': problem_location,
'student_id': grader_id,
'calibration_essay_id': calibration_essay_id,
'submission_key': submission_key,
'score': score,
'feedback': feedback,
'rubric_scores[]': rubric_scores,
'rubric_scores_complete': True}
return self.try_to_decode(self.post(self.save_calibration_essay_url, data))
def get_problem_list(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_problem_list_url, params)
return self.try_to_decode(response)
def get_notifications(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_notifications_url, params)
return self.try_to_decode(response)
def try_to_decode(self, text):
try:
text = json.loads(text)
except:
pass
return text
"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
"""
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key):
return json.dumps({'success': True})
def is_student_calibrated(self, problem_location, grader_id):
return json.dumps({'success': True, 'calibrated': True})
def show_calibration_essay(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score, feedback):
return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id):
return json.dumps({'success': True,
'problem_list': [
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
]})
_service = None
def peer_grading_service(system):
"""
Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True,
returns a mock one, otherwise a real one.
Caches the result, so changing the setting after the first call to this
function will have no effect.
"""
global _service
if _service is not None:
return _service
if settings.MOCK_PEER_GRADING:
_service = MockPeerGradingService()
else:
_service = PeerGradingService(settings.PEER_GRADING_INTERFACE, system)
return _service
...@@ -6,7 +6,7 @@ from pkg_resources import resource_string, resource_listdir ...@@ -6,7 +6,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
...@@ -121,12 +121,12 @@ class VideoModule(XModule): ...@@ -121,12 +121,12 @@ class VideoModule(XModule):
return self.youtube return self.youtube
def get_html(self): def get_html(self):
if isinstance(modulestore(), MongoModuleStore) : if isinstance(modulestore(), XMLModuleStore) :
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat] # VS[compat]
# cdodge: filesystem static content support. # cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir']) caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
else:
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'streams': self.video_list(),
......
<peergrading display_name = "Peer Grading" use_for_single_location="False" is_graded="False"/>
# .coveragerc for lms # .coveragerc for lms
[run] [run]
data_file = reports/lms/.coverage data_file = reports/lms/.coverage
source = lms source = lms,common/djangoapps
omit = lms/envs/* omit = lms/envs/*
[report] [report]
......
...@@ -19,7 +19,7 @@ from xmodule.contentstore.content import StaticContent ...@@ -19,7 +19,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModule from xmodule.x_module import XModule
from static_replace import replace_urls, try_staticfiles_lookup from static_replace import replace_static_urls
from courseware.access import has_access from courseware.access import has_access
import branding import branding
from courseware.models import StudentModuleCache from courseware.models import StudentModuleCache
...@@ -83,13 +83,12 @@ def get_opt_course_with_access(user, course_id, action): ...@@ -83,13 +83,12 @@ def get_opt_course_with_access(user, course_id, action):
return None return None
return get_course_with_access(user, course_id, action) return get_course_with_access(user, course_id, action)
def course_image_url(course): def course_image_url(course):
"""Try to look up the image url for the course. If it's not found, """Try to look up the image url for the course. If it's not found,
log an error and return the dead link""" log an error and return the dead link"""
if isinstance(modulestore(), XMLModuleStore): if isinstance(modulestore(), XMLModuleStore):
path = course.metadata['data_dir'] + "/images/course_image.jpg" return '/static/' + course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
else: else:
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg') loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
path = StaticContent.get_url_path_from_location(loc) path = StaticContent.get_url_path_from_location(loc)
...@@ -224,8 +223,11 @@ def get_course_syllabus_section(course, section_key): ...@@ -224,8 +223,11 @@ def get_course_syllabus_section(course, section_key):
dirs = [path("syllabus") / course.url_name, path("syllabus")] dirs = [path("syllabus") / course.url_name, path("syllabus")]
filepath = find_file(fs, dirs, section_key + ".html") filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile: with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'), return replace_static_urls(
course.metadata['data_dir'], course_namespace=course.location) htmlFile.read().decode('utf-8'),
course.metadata['data_dir'],
course_namespace=course.location
)
except ResourceNotFoundError: except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format( log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url())) key=section_key, url=course.location.url()))
......
...@@ -2,6 +2,9 @@ import json ...@@ -2,6 +2,9 @@ import json
import logging import logging
import pyparsing import pyparsing
import sys import sys
import static_replace
from functools import partial
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -18,7 +21,6 @@ from courseware.access import has_access ...@@ -18,7 +21,6 @@ from courseware.access import has_access
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache from models import StudentModule, StudentModuleCache
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from static_replace import replace_urls
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -244,7 +246,11 @@ def _get_module(user, request, descriptor, student_module_cache, course_id, ...@@ -244,7 +246,11 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
# TODO (cpennington): This should be removed when all html from # TODO (cpennington): This should be removed when all html from
# a module is coming through get_html and is therefore covered # a module is coming through get_html and is therefore covered
# by the replace_static_urls code below # by the replace_static_urls code below
replace_urls=replace_urls, replace_urls=partial(
static_replace.replace_static_urls,
data_directory=descriptor.metadata.get('data_dir', ''),
course_namespace=descriptor.location._replace(category=None, name=None),
),
node_path=settings.NODE_PATH, node_path=settings.NODE_PATH,
anonymous_student_id=unique_id_for_user(user), anonymous_student_id=unique_id_for_user(user),
course_id=course_id, course_id=course_id,
...@@ -280,7 +286,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id, ...@@ -280,7 +286,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
_get_html, _get_html,
module.metadata['data_dir'] if 'data_dir' in module.metadata else '', module.metadata.get('data_dir', ''),
course_namespace = module.location._replace(category=None, name=None)) course_namespace = module.location._replace(category=None, name=None))
# Allow URLs of the form '/course/' refer to the root of multicourse directory # Allow URLs of the form '/course/' refer to the root of multicourse directory
......
...@@ -19,12 +19,10 @@ from django.core.urlresolvers import reverse ...@@ -19,12 +19,10 @@ from django.core.urlresolvers import reverse
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from courseware.access import has_access from courseware.access import has_access
from static_replace import replace_urls
from lxml.html import rewrite_links from lxml.html import rewrite_links
from module_render import get_module from module_render import get_module
from courseware.access import has_access from courseware.access import has_access
from static_replace import replace_urls
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
...@@ -322,4 +320,4 @@ def get_static_tab_contents(request, cache, course, tab): ...@@ -322,4 +320,4 @@ def get_static_tab_contents(request, cache, course, tab):
if tab_module is not None: if tab_module is not None:
html = tab_module.get_html() html = tab_module.get_html()
return html return html
\ No newline at end of file
...@@ -12,6 +12,9 @@ import pystache_custom as pystache ...@@ -12,6 +12,9 @@ import pystache_custom as pystache
import urllib import urllib
import os import os
# This method is used to pluralize the words "discussion" and "comment"
# when referring to how many discussion threads or comments the user
# has contributed to.
def pluralize(singular_term, count): def pluralize(singular_term, count):
if int(count) >= 2 or int(count) == 0: if int(count) >= 2 or int(count) == 0:
return singular_term + 's' return singular_term + 's'
......
...@@ -46,11 +46,13 @@ class Role(models.Model): ...@@ -46,11 +46,13 @@ class Role(models.Model):
def add_permission(self, permission): def add_permission(self, permission):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0]) self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission): def has_permission(self, permission):
course = get_course_by_id(self.course_id) course = get_course_by_id(self.course_id)
if self.name == FORUM_ROLE_STUDENT and \ changing_comments = permission.startswith('edit') or \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \ permission.startswith('update') or permission.startswith('create')
(not course.forum_posts_allowed): in_blackout_period = not course.forum_posts_allowed
if (self.name == FORUM_ROLE_STUDENT) and in_blackout_period and changing_comments:
return False return False
return self.permissions.filter(name=permission).exists() return self.permissions.filter(name=permission).exists()
......
...@@ -5,6 +5,8 @@ import urllib ...@@ -5,6 +5,8 @@ import urllib
import sys import sys
import inspect import inspect
# This method is used to pluralize the words "discussion" and "comment"
# which is why you need to tack on an "s" for the case of 0 or two or more.
def pluralize(content, text): def pluralize(content, text):
num, word = text.split(' ') num, word = text.split(' ')
num = int(num or '0') num = int(num or '0')
......
...@@ -29,6 +29,7 @@ def has_permission(user, permission, course_id=None): ...@@ -29,6 +29,7 @@ def has_permission(user, permission, course_id=None):
CONDITIONS = ['is_open', 'is_author'] CONDITIONS = ['is_open', 'is_author']
# data may be a json file
def check_condition(user, condition, course_id, data): def check_condition(user, condition, course_id, data):
def check_open(user, condition, course_id, data): def check_open(user, condition, course_id, data):
try: try:
...@@ -61,8 +62,10 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs): ...@@ -61,8 +62,10 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
def test(user, per, operator="or"): def test(user, per, operator="or"):
if isinstance(per, basestring): if isinstance(per, basestring):
if per in CONDITIONS: if per in CONDITIONS:
return check_condition(user, per, course_id, kwargs) return check_condition(user, per, course_id, kwargs['data'])
return cached_has_permission(user, per, course_id=course_id) return cached_has_permission(user, per, course_id=course_id)
# TODO: refactor this to be more clear.
# e.g. the "and operator in" bit on the next line is not needed?
elif isinstance(per, list) and operator in ["and", "or"]: elif isinstance(per, list) and operator in ["and", "or"]:
results = [test(user, x, operator="and") for x in per] results = [test(user, x, operator="and") for x in per]
if operator == "or": if operator == "or":
...@@ -102,4 +105,4 @@ def check_permissions_by_view(user, course_id, content, name): ...@@ -102,4 +105,4 @@ def check_permissions_by_view(user, course_id, content, name):
p = VIEW_PERMISSIONS[name] p = VIEW_PERMISSIONS[name]
except KeyError: except KeyError:
logging.warning("Permission for view named %s does not exist in permissions.py" % name) logging.warning("Permission for view named %s does not exist in permissions.py" % name)
return check_conditions_permissions(user, p, course_id, content=content) return check_conditions_permissions(user, p, course_id, data=content)
from django.contrib.auth.models import User, Group
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from mock import Mock
from override_settings import override_settings
import xmodule.modulestore.django
from student.models import CourseEnrollment
from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save
from django.dispatch.dispatcher import _make_id
import string
import random
from .permissions import has_permission
from .models import Role, Permission
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore
import comment_client
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
#class TestCohorting(PageLoader):
# """Check that cohorting works properly"""
#
# def setUp(self):
# xmodule.modulestore.django._MODULESTORES = {}
#
# # Assume courses are there
# self.toy = modulestore().get_course("edX/toy/2012_Fall")
#
# # Create two accounts
# self.student = 'view@test.com'
# self.student2 = 'view2@test.com'
# self.password = 'foo'
# self.create_account('u1', self.student, self.password)
# self.create_account('u2', self.student2, self.password)
# self.activate_user(self.student)
# self.activate_user(self.student2)
#
# def test_create_thread(self):
# my_save = Mock()
# comment_client.perform_request = my_save
#
# resp = self.client.post(
# reverse('django_comment_client.base.views.create_thread',
# kwargs={'course_id': 'edX/toy/2012_Fall',
# 'commentable_id': 'General'}),
# {'some': "some",
# 'data': 'data'})
# self.assertTrue(my_save.called)
#
# #self.assertEqual(resp.status_code, 200)
# #self.assertEqual(my_save.something, "expected", "complaint if not true")
#
# self.toy.metadata["cohort_config"] = {"cohorted": True}
#
# # call the view again ...
#
# # assert that different things happened
class PermissionsTestCase(TestCase):
def random_str(self, length=15, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(length))
def setUp(self):
self.course_id = "edX/toy/2012_Fall"
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
self.student_role = Role.objects.get_or_create(name="Student", course_id=self.course_id)[0]
self.student = User.objects.create(username=self.random_str(),
password="123456", email="john@yahoo.com")
self.moderator = User.objects.create(username=self.random_str(),
password="123456", email="staff@edx.org")
self.moderator.is_staff = True
self.moderator.save()
self.student_enrollment = CourseEnrollment.objects.create(user=self.student, course_id=self.course_id)
self.moderator_enrollment = CourseEnrollment.objects.create(user=self.moderator, course_id=self.course_id)
def tearDown(self):
self.student_enrollment.delete()
self.moderator_enrollment.delete()
# Do we need to have this? We shouldn't be deleting students, ever
# self.student.delete()
# self.moderator.delete()
def testDefaultRoles(self):
self.assertTrue(self.student_role in self.student.roles.all())
self.assertTrue(self.moderator_role in self.moderator.roles.all())
def testPermission(self):
name = self.random_str()
self.moderator_role.add_permission(name)
self.assertTrue(has_permission(self.moderator, name, self.course_id))
self.student_role.add_permission(name)
self.assertTrue(has_permission(self.student, name, self.course_id))
import string
import random
import collections
from django.test import TestCase
from django_comment_client.helpers import pluralize
class PluralizeTestCase(TestCase):
def testPluralize(self):
self.term = "cat"
self.assertEqual(pluralize(self.term, 0), "cats")
self.assertEqual(pluralize(self.term, 1), "cat")
self.assertEqual(pluralize(self.term, 2), "cats")
import string
import random
import collections
from django.test import TestCase
import comment_client
import django.http
import django_comment_client.middleware as middleware
class AjaxExceptionTestCase(TestCase):
# TODO: check whether the correct error message is produced.
# The error message should be the same as the argument to CommentClientError
def setUp(self):
self.a = middleware.AjaxExceptionMiddleware()
self.request1 = django.http.HttpRequest()
self.request0 = django.http.HttpRequest()
self.exception1 = comment_client.CommentClientError('{}')
self.exception0 = ValueError()
self.request1.META['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
self.request0.META['HTTP_X_REQUESTED_WITH'] = "SHADOWFAX"
def test_process_exception(self):
self.assertIsInstance(self.a.process_exception(self.request1, self.exception1), middleware.JsonError)
self.assertIsNone(self.a.process_exception(self.request1, self.exception0))
self.assertIsNone(self.a.process_exception(self.request0, self.exception1))
self.assertIsNone(self.a.process_exception(self.request0, self.exception0))
import django_comment_client.models as models
import django_comment_client.permissions as permissions
from django.test import TestCase
from nose.plugins.skip import SkipTest
from courseware.courses import get_course_by_id
class RoleClassTestCase(TestCase):
def setUp(self):
self.course_id = "edX/toy/2012_Fall"
self.student_role = models.Role.objects.create(name="Student",
course_id=self.course_id)
def test_unicode(self):
self.assertEqual(str(self.student_role), "Student for edX/toy/2012_Fall")
self.admin_for_all = models.Role.objects.create(name="Administrator")
self.assertEqual(str(self.admin_for_all), "Administrator for all courses")
def test_has_permission(self):
self.student_role.add_permission("delete_thread")
self.TA_role = models.Role.objects.create(name="Community TA",
course_id=self.course_id)
self.assertTrue(self.student_role.has_permission("delete_thread"))
self.assertFalse(self.TA_role.has_permission("delete_thread"))
# Toy course does not have a blackout period defined.
def test_students_can_create_if_not_during_blackout(self):
self.student_role.add_permission("create_comment")
self.assertTrue(self.student_role.has_permission("create_comment"))
def test_students_cannot_create_during_blackout(self):
# Not sure how to set up these conditions
raise SkipTest()
def test_inherit_permissions(self):
self.student_role.add_permission("delete_thread")
self.TA_role = models.Role.objects.create(name="Community TA",
course_id=self.course_id)
self.TA_role.inherit_permissions(self.student_role)
self.assertTrue(self.TA_role.has_permission("delete_thread"))
# TODO: You should not be able to inherit permissions across courses?
def test_inherit_permissions_across_courses(self):
raise SkipTest()
self.student_role.add_permission("delete_thread")
self.course_id_2 = "MITx/6.002x/2012_Fall"
self.admin_role = models.Role.objects.create(name="Administrator",
course_id=self.course_id_2)
self.admin_role.inherit_permissions(self.student_role)
class PermissionClassTestCase(TestCase):
def test_unicode(self):
self.permission = permissions.Permission.objects.create(name="test")
self.assertEqual(str(self.permission), "test")
import string
import random
import collections
from django.test import TestCase
import django_comment_client.mustache_helpers as mustache_helpers
class PluralizeTestCase(TestCase):
def test_pluralize(self):
self.text1 = '0 goat'
self.text2 = '1 goat'
self.text3 = '7 goat'
self.content = 'unused argument'
self.assertEqual(mustache_helpers.pluralize(self.content, self.text1), 'goats')
self.assertEqual(mustache_helpers.pluralize(self.content, self.text2), 'goat')
self.assertEqual(mustache_helpers.pluralize(self.content, self.text3), 'goats')
class CloseThreadTextTestCase(TestCase):
def test_close_thread_text(self):
self.contentClosed = {'closed': True}
self.contentOpen = {'closed': False}
self.assertEqual(mustache_helpers.close_thread_text(self.contentClosed), 'Re-open thread')
self.assertEqual(mustache_helpers.close_thread_text(self.contentOpen), 'Close thread')
import string
import random
import collections
import factory
from django.test import TestCase
from django.contrib.auth.models import User
from student.models import UserProfile, CourseEnrollment
from django_comment_client.models import Role, Permission
import django_comment_client.permissions as p
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot'
password = '123456'
email = 'robot@edx.org'
is_active = True
is_staff = False
class CourseEnrollmentFactory(factory.Factory):
FACTORY_FOR = CourseEnrollment
user = factory.SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall'
class RoleFactory(factory.Factory):
FACTORY_FOR = Role
name = 'Student'
course_id = 'edX/toy/2012_Fall'
class PermissionFactory(factory.Factory):
FACTORY_FOR = Permission
name = 'create_comment'
class PermissionsTestCase(TestCase):
def setUp(self):
self.course_id = "edX/toy/2012_Fall"
self.student_role = RoleFactory(name='Student')
self.moderator_role = RoleFactory(name='Moderator')
self.student = UserFactory(username='student', email='student@edx.org')
self.moderator = UserFactory(username='moderator', email='staff@edx.org', is_staff=True)
self.update_thread_permission = PermissionFactory(name='update_thread')
self.update_thread_permission.roles.add(self.student_role)
self.update_thread_permission.roles.add(self.moderator_role)
self.manage_moderator_permission = PermissionFactory(name='manage_moderator')
self.manage_moderator_permission.roles.add(self.moderator_role)
self.student_enrollment = CourseEnrollmentFactory(user=self.student)
self.moderator_enrollment = CourseEnrollmentFactory(user=self.moderator)
self.student_open_thread = {'content': {
'closed': False,
'user_id': str(self.student.id)}
}
self.student_closed_thread = {'content': {
'closed': True,
'user_id': str(self.student.id)}
}
def test_user_has_permission(self):
s_ut = p.has_permission(self.student, 'update_thread', self.course_id)
m_ut = p.has_permission(self.moderator, 'update_thread', self.course_id)
s_mm = p.has_permission(self.student, 'manage_moderator', self.course_id)
m_mm = p.has_permission(self.moderator, 'manage_moderator', self.course_id)
self.assertTrue(s_ut)
self.assertTrue(m_ut)
self.assertFalse(s_mm)
self.assertTrue(m_mm)
def test_check_conditions(self):
# Checks whether the discussion thread is open, or whether the author is user
s_o = p.check_condition(self.student, 'is_open', self.course_id, self.student_open_thread)
s_a = p.check_condition(self.student, 'is_author', self.course_id, self.student_open_thread)
m_c = p.check_condition(self.moderator, 'is_open', self.course_id, self.student_closed_thread)
m_a = p.check_condition(self.moderator,'is_author', self.course_id, self.student_open_thread)
self.assertTrue(s_o)
self.assertTrue(s_a)
self.assertFalse(m_c)
self.assertFalse(m_a)
def test_check_conditions_and_permissions(self):
# Check conditions
ret = p.check_conditions_permissions(self.student,
'is_open',
self.course_id,
data=self.student_open_thread)
self.assertTrue(ret)
# Check permissions
ret = p.check_conditions_permissions(self.student,
'update_thread',
self.course_id,
data=self.student_open_thread)
self.assertTrue(ret)
# Check that a list of permissions/conditions will be OR'd
ret = p.check_conditions_permissions(self.moderator,
['is_open','manage_moderator'],
self.course_id,
data=self.student_open_thread)
self.assertTrue(ret)
# Check that a list of permissions will be OR'd
ret = p.check_conditions_permissions(self.student,
['update_thread','manage_moderator'],
self.course_id,
data=self.student_open_thread)
self.assertTrue(ret)
# Check that a list of list of permissions will be AND'd
ret = p.check_conditions_permissions(self.student,
[['update_thread','manage_moderator']],
self.course_id,
data=self.student_open_thread)
self.assertFalse(ret)
def test_check_permissions_by_view(self):
ret = p.check_permissions_by_view(self.student, self.course_id,
self.student_open_thread, 'openclose_thread')
self.assertFalse(ret)
# Check a view permission that includes both a condition and a permission
self.vote_permission = PermissionFactory(name='vote')
self.vote_permission.roles.add(self.student_role)
ret = p.check_permissions_by_view(self.student, self.course_id,
self.student_open_thread, 'vote_for_comment')
self.assertTrue(ret)
\ No newline at end of file
import string
import random
import collections
from django.test import TestCase
import factory
from django.contrib.auth.models import User
from student.models import UserProfile, CourseEnrollment
from django_comment_client.models import Role, Permission
import django_comment_client.models as models
import django_comment_client.utils as utils
import xmodule.modulestore.django as django
class UserFactory(factory.Factory):
FACTORY_FOR = User
username = 'robot'
password = '123456'
email = 'robot@edx.org'
is_active = True
is_staff = False
class CourseEnrollmentFactory(factory.Factory):
FACTORY_FOR = CourseEnrollment
user = factory.SubFactory(UserFactory)
course_id = 'edX/toy/2012_Fall'
class RoleFactory(factory.Factory):
FACTORY_FOR = Role
name = 'Student'
course_id = 'edX/toy/2012_Fall'
class PermissionFactory(factory.Factory):
FACTORY_FOR = Permission
name = 'create_comment'
class DictionaryTestCase(TestCase):
def test_extract(self):
d = {'cats': 'meow', 'dogs': 'woof'}
k = ['cats', 'dogs', 'hamsters']
expected = {'cats': 'meow', 'dogs': 'woof', 'hamsters': None}
self.assertEqual(utils.extract(d, k), expected)
def test_strip_none(self):
d = {'cats': 'meow', 'dogs': 'woof', 'hamsters': None}
expected = {'cats': 'meow', 'dogs': 'woof'}
self.assertEqual(utils.strip_none(d), expected)
def test_strip_blank(self):
d = {'cats': 'meow', 'dogs': 'woof', 'hamsters': ' ', 'yetis': ''}
expected = {'cats': 'meow', 'dogs': 'woof'}
self.assertEqual(utils.strip_blank(d), expected)
def test_merge_dict(self):
d1 ={'cats': 'meow', 'dogs': 'woof'}
d2 ={'lions': 'roar','ducks': 'quack'}
expected ={'cats': 'meow', 'dogs': 'woof','lions': 'roar','ducks': 'quack'}
self.assertEqual(utils.merge_dict(d1, d2), expected)
class AccessUtilsTestCase(TestCase):
def setUp(self):
self.course_id = 'edX/toy/2012_Fall'
self.student_role = RoleFactory(name='Student', course_id=self.course_id)
self.moderator_role = RoleFactory(name='Moderator', course_id=self.course_id)
self.student1 = UserFactory(username='student', email='student@edx.org')
self.student1_enrollment = CourseEnrollmentFactory(user=self.student1)
self.student_role.users.add(self.student1)
self.student2 = UserFactory(username='student2', email='student2@edx.org')
self.student2_enrollment = CourseEnrollmentFactory(user=self.student2)
self.moderator = UserFactory(username='moderator', email='staff@edx.org', is_staff=True)
self.moderator_enrollment = CourseEnrollmentFactory(user=self.moderator)
self.moderator_role.users.add(self.moderator)
def test_get_role_ids(self):
ret = utils.get_role_ids(self.course_id)
expected = {u'Moderator': [3], u'Student': [1, 2], 'Staff': [3]}
self.assertEqual(ret, expected)
def test_has_forum_access(self):
ret = utils.has_forum_access('student', self.course_id, 'Student')
self.assertTrue(ret)
ret = utils.has_forum_access('not_a_student', self.course_id, 'Student')
self.assertFalse(ret)
ret = utils.has_forum_access('student', self.course_id, 'NotARole')
self.assertFalse(ret)
...@@ -35,6 +35,7 @@ def strip_blank(dic): ...@@ -35,6 +35,7 @@ def strip_blank(dic):
return isinstance(v, str) and len(v.strip()) == 0 return isinstance(v, str) and len(v.strip()) == 0
return dict([(k, v) for k, v in dic.iteritems() if not _is_blank(v)]) return dict([(k, v) for k, v in dic.iteritems() if not _is_blank(v)])
# TODO should we be checking if d1 and d2 have the same keys with different values?
def merge_dict(dic1, dic2): def merge_dict(dic1, dic2):
return dict(dic1.items() + dic2.items()) return dict(dic1.items() + dic2.items())
......
...@@ -3,11 +3,12 @@ import logging ...@@ -3,11 +3,12 @@ import logging
import requests import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from grading_service import GradingService from xmodule.grading_service_module import GradingService, GradingServiceError
from grading_service import GradingServiceError
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -16,6 +17,7 @@ class ControllerQueryService(GradingService): ...@@ -16,6 +17,7 @@ class ControllerQueryService(GradingService):
Interface to staff grading backend. Interface to staff grading backend.
""" """
def __init__(self, config): def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(ControllerQueryService, self).__init__(config) super(ControllerQueryService, self).__init__(config)
self.check_eta_url = self.url + '/get_submission_eta/' self.check_eta_url = self.url + '/get_submission_eta/'
self.is_unique_url = self.url + '/is_name_unique/' self.is_unique_url = self.url + '/is_name_unique/'
......
from django.conf import settings from django.conf import settings
from staff_grading_service import StaffGradingService from staff_grading_service import StaffGradingService
from peer_grading_service import PeerGradingService
from open_ended_grading.controller_query_service import ControllerQueryService from open_ended_grading.controller_query_service import ControllerQueryService
import json import json
from student.models import unique_id_for_user from student.models import unique_id_for_user
...@@ -10,6 +9,7 @@ import logging ...@@ -10,6 +9,7 @@ import logging
from courseware.access import has_access from courseware.access import has_access
from util.cache import cache from util.cache import cache
import datetime import datetime
from xmodule import peer_grading_service
log=logging.getLogger(__name__) log=logging.getLogger(__name__)
......
"""
This module provides an interface on the grading-service backend
for peer grading
Use peer_grading_service() to get the version specified
in settings.PEER_GRADING_INTERFACE
"""
import json
import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
from django.conf import settings
from django.http import HttpResponse, Http404
from grading_service import GradingService
from grading_service import GradingServiceError
from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric
from student.models import unique_id_for_user
from lxml import etree
log = logging.getLogger(__name__)
"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
"""
class MockPeerGradingService(object):
# TODO: get this rubric parsed and working
rubric = """<rubric>
<category>
<description>Description</description>
<option>First option</option>
<option>Second option</option>
</category>
</rubric>"""
def get_next_submission(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged):
return json.dumps({'success': True})
def is_student_calibrated(self, problem_location, grader_id):
return json.dumps({'success': True, 'calibrated': True})
def show_calibration_essay(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score, feedback, rubric_scores):
return json.dumps({'success': True, 'actual_score': 2})
def get_problem_list(self, course_id, grader_id):
return json.dumps({'success': True,
'problem_list': [
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'num_required': 7}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'num_required': 8})
]})
class PeerGradingService(GradingService):
"""
Interface with the grading controller for peer grading
"""
def __init__(self, config):
super(PeerGradingService, self).__init__(config)
self.get_next_submission_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/'
self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
self.get_problem_list_url = self.url + '/get_problem_list/'
self.get_notifications_url = self.url + '/get_notifications/'
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id})
return json.dumps(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
data = {'grader_id' : grader_id,
'submission_id' : submission_id,
'score' : score,
'feedback' : feedback,
'submission_key': submission_key,
'location': location,
'rubric_scores': rubric_scores,
'rubric_scores_complete': True,
'submission_flagged' : submission_flagged}
return self.post(self.save_grade_url, data)
def is_student_calibrated(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.get(self.is_student_calibrated_url, params)
def show_calibration_essay(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
response = self.get(self.show_calibration_essay_url, params)
return json.dumps(self._render_rubric(response))
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key,
score, feedback, rubric_scores):
data = {'location': problem_location,
'student_id': grader_id,
'calibration_essay_id': calibration_essay_id,
'submission_key': submission_key,
'score': score,
'feedback': feedback,
'rubric_scores[]': rubric_scores,
'rubric_scores_complete': True}
return self.post(self.save_calibration_essay_url, data)
def get_problem_list(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_problem_list_url, params)
return response
def get_notifications(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_notifications_url, params)
return response
_service = None
def peer_grading_service():
"""
Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True,
returns a mock one, otherwise a real one.
Caches the result, so changing the setting after the first call to this
function will have no effect.
"""
global _service
if _service is not None:
return _service
if settings.MOCK_PEER_GRADING:
_service = MockPeerGradingService()
else:
_service = PeerGradingService(settings.PEER_GRADING_INTERFACE)
return _service
def _err_response(msg):
"""
Return a HttpResponse with a json dump with success=False, and the given error message.
"""
return HttpResponse(json.dumps({'success': False, 'error': msg}),
mimetype="application/json")
def _check_required(request, required):
actual = set(request.POST.keys())
missing = required - actual
if len(missing) > 0:
return False, "Missing required keys: {0}".format(', '.join(missing))
else:
return True, ""
def _check_post(request):
if request.method != 'POST':
raise Http404
def get_next_submission(request, course_id):
"""
Makes a call to the grading controller for the next essay that should be graded
Returns a json dict with the following keys:
'success': bool
'submission_id': a unique identifier for the submission, to be passed back
with the grade.
'submission': the submission, rendered as read-only html for grading
'rubric': the rubric, also rendered as html.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().get_next_submission(location, grader_id)
return HttpResponse(response,
mimetype="application/json")
except GradingServiceError:
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(peer_grading_service().url, location, grader_id))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def save_grade(request, course_id):
"""
Saves the grade of a given submission.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
"""
_check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
submission_id = p['submission_id']
score = p['score']
feedback = p['feedback']
submission_key = p['submission_key']
rubric_scores = p.getlist('rubric_scores[]')
submission_flagged = p['submission_flagged']
try:
response = peer_grading_service().save_grade(location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}"""
.format(peer_grading_service().url,
location, submission_id, submission_key, score)
)
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def is_student_calibrated(request, course_id):
"""
Calls the grading controller to see if the given student is calibrated
on the given problem
Input:
In the request, we need the following arguments:
location - problem location
Returns:
Json object with the following keys
success - bool indicating whether or not the call was successful
calibrated - true if the grader has fully calibrated and can now move on to grading
- false if the grader is still working on calibration problems
total_calibrated_on_so_far - the number of calibration essays for this problem
that this grader has graded
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().is_student_calibrated(location, grader_id)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(peer_grading_service().url, grader_id, location))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
def show_calibration_essay(request, course_id):
"""
Fetch the next calibration essay from the grading controller and return it
Inputs:
In the request
location - problem location
Returns:
A json dict with the following keys
'success': bool
'submission_id': a unique identifier for the submission, to be passed back
with the grade.
'submission': the submission, rendered as read-only html for grading
'rubric': the rubric, also rendered as html.
'submission_key': a key associated with the submission for validation reasons
'error': if success is False, will have an error message with more info.
"""
_check_post(request)
required = set(['location'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
try:
response = peer_grading_service().show_calibration_essay(location, grader_id)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}, location: {0}"
.format(peer_grading_service().url, location))
return json.dumps({'success': False,
'error': 'Could not connect to grading service'})
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError:
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
return json.dumps({'success': False,
'error': 'Error displaying submission'})
def save_calibration_essay(request, course_id):
"""
Saves the grader's grade of a given calibration.
Input:
The request should have the following keys:
location - problem location
submission_id - id associated with this submission
submission_key - submission key given for validation purposes
score - the grade that was given to the submission
feedback - the feedback from the student
Returns
A json object with the following keys:
success: bool indicating whether the save was a success
error: if there was an error in the submission, this is the error message
actual_score: the score that the instructor gave to this calibration essay
"""
_check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
success, message = _check_required(request, required)
if not success:
return _err_response(message)
grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location']
calibration_essay_id = p['submission_id']
submission_key = p['submission_key']
score = p['score']
feedback = p['feedback']
rubric_scores = p.getlist('rubric_scores[]')
try:
response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores)
return HttpResponse(response, mimetype="application/json")
except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
return _err_response('Could not connect to grading service')
...@@ -7,8 +7,7 @@ import logging ...@@ -7,8 +7,7 @@ import logging
import requests import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from grading_service import GradingService from xmodule.grading_service_module import GradingService, GradingServiceError
from grading_service import GradingServiceError
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
...@@ -22,8 +21,6 @@ from mitxmako.shortcuts import render_to_string ...@@ -22,8 +21,6 @@ from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MockStaffGradingService(object): class MockStaffGradingService(object):
""" """
A simple mockup of a staff grading service, testing. A simple mockup of a staff grading service, testing.
...@@ -64,6 +61,7 @@ class StaffGradingService(GradingService): ...@@ -64,6 +61,7 @@ class StaffGradingService(GradingService):
Interface to staff grading backend. Interface to staff grading backend.
""" """
def __init__(self, config): def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(StaffGradingService, self).__init__(config) super(StaffGradingService, self).__init__(config)
self.get_next_url = self.url + '/get_next_submission/' self.get_next_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/' self.save_grade_url = self.url + '/save_grade/'
......
...@@ -6,7 +6,7 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open ...@@ -6,7 +6,7 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open
from django.test import TestCase from django.test import TestCase
from open_ended_grading import staff_grading_service from open_ended_grading import staff_grading_service
from open_ended_grading import peer_grading_service from xmodule import peer_grading_service, peer_grading_module
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
...@@ -17,10 +17,13 @@ import xmodule.modulestore.django ...@@ -17,10 +17,13 @@ import xmodule.modulestore.django
from nose import SkipTest from nose import SkipTest
from mock import patch, Mock from mock import patch, Mock
import json import json
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from override_settings import override_settings from override_settings import override_settings
from django.http import QueryDict
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
...@@ -98,6 +101,7 @@ class TestStaffGradingService(ct.PageLoader): ...@@ -98,6 +101,7 @@ class TestStaffGradingService(ct.PageLoader):
'submission_id': '123', 'submission_id': '123',
'location': self.location, 'location': self.location,
'rubric_scores[]': ['1', '2']} 'rubric_scores[]': ['1', '2']}
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(d['success'], str(d))
...@@ -136,19 +140,21 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -136,19 +140,21 @@ class TestPeerGradingService(ct.PageLoader):
self.course_id = "edX/toy/2012_Fall" self.course_id = "edX/toy/2012_Fall"
self.toy = modulestore().get_course(self.course_id) self.toy = modulestore().get_course(self.course_id)
location = "i4x://edX/toy/peergrading/init"
self.mock_service = peer_grading_service.peer_grading_service() self.mock_service = peer_grading_service.MockPeerGradingService()
self.system = ModuleSystem(location, None, None, render_to_string, None)
self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system)
self.peer_module = peer_grading_module.PeerGradingModule(self.system,location,"<peergrading/>",self.descriptor)
self.peer_module.peer_gs = self.mock_service
self.logout() self.logout()
def test_get_next_submission_success(self): def test_get_next_submission_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_get_next_submission', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.get_next_submission(data)
d = json.loads(r.content) d = json.loads(r)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(d['prompt'])
...@@ -156,63 +162,48 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -156,63 +162,48 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(d['max_score'])
def test_get_next_submission_missing_location(self): def test_get_next_submission_missing_location(self):
self.login(self.student, self.password)
url = reverse('peer_grading_get_next_submission', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.get_next_submission(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location") self.assertEqual(d['error'], "Missing required keys: location")
def test_save_grade_success(self): def test_save_grade_success(self):
self.login(self.student, self.password) raise SkipTest()
url = reverse('peer_grading_save_grade', kwargs={'course_id': self.course_id}) data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
data = {'location': self.location, qdict = QueryDict(data.replace("|","&"))
'submission_id': '1', r = self.peer_module.save_grade(qdict)
'submission_key': 'fake key', d = r
'score': '2',
'feedback': 'This is feedback',
'rubric_scores[]': [1, 2],
'submission_flagged' : False}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success']) self.assertTrue(d['success'])
def test_save_grade_missing_keys(self): def test_save_grade_missing_keys(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_grade', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.save_grade(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1) self.assertTrue(d['error'].find('Missing required keys:') > -1)
def test_is_calibrated_success(self): def test_is_calibrated_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_is_student_calibrated', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.is_student_calibrated(data)
d = json.loads(r.content) d = json.loads(r)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertTrue('calibrated' in d) self.assertTrue('calibrated' in d)
def test_is_calibrated_failure(self): def test_is_calibrated_failure(self):
self.login(self.student, self.password)
url = reverse('peer_grading_is_student_calibrated', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.is_student_calibrated(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertFalse('calibrated' in d) self.assertFalse('calibrated' in d)
def test_show_calibration_essay_success(self): def test_show_calibration_essay_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_show_calibration_essay', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.show_calibration_essay(data)
d = json.loads(r.content) d = json.loads(r)
log.debug(d)
log.debug(type(d))
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(d['prompt'])
...@@ -220,37 +211,27 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -220,37 +211,27 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(d['max_score'])
def test_show_calibration_essay_missing_key(self): def test_show_calibration_essay_missing_key(self):
self.login(self.student, self.password)
url = reverse('peer_grading_show_calibration_essay', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.show_calibration_essay(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location") self.assertEqual(d['error'], "Missing required keys: location")
def test_save_calibration_essay_success(self): def test_save_calibration_essay_success(self):
self.login(self.student, self.password) raise SkipTest()
url = reverse('peer_grading_save_calibration_essay', kwargs={'course_id': self.course_id}) data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
data = {'location': self.location, qdict = QueryDict(data.replace("|","&"))
'submission_id': '1', r = self.peer_module.save_calibration_essay(qdict)
'submission_key': 'fake key', d = r
'score': '2',
'feedback': 'This is feedback',
'rubric_scores[]': [1, 2]}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertTrue('actual_score' in d) self.assertTrue('actual_score' in d)
def test_save_calibration_essay_missing_keys(self): def test_save_calibration_essay_missing_keys(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_calibration_essay', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.save_calibration_essay(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1) self.assertTrue(d['error'].find('Missing required keys:') > -1)
self.assertFalse('actual_score' in d) self.assertFalse('actual_score' in d)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import logging import logging
import urllib import urllib
import re
from django.conf import settings from django.conf import settings
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
...@@ -11,10 +12,8 @@ from django.core.urlresolvers import reverse ...@@ -11,10 +12,8 @@ from django.core.urlresolvers import reverse
from student.models import unique_id_for_user from student.models import unique_id_for_user
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from peer_grading_service import PeerGradingService
from peer_grading_service import MockPeerGradingService
from controller_query_service import ControllerQueryService from controller_query_service import ControllerQueryService
from grading_service import GradingServiceError from xmodule.grading_service_module import GradingServiceError
import json import json
from .staff_grading import StaffGrading from .staff_grading import StaffGrading
from student.models import unique_id_for_user from student.models import unique_id_for_user
...@@ -25,15 +24,11 @@ import open_ended_notifications ...@@ -25,15 +24,11 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search from xmodule.modulestore import search
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404, HttpResponseRedirect
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
if settings.MOCK_PEER_GRADING:
peer_gs = MockPeerGradingService()
else:
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
controller_url = open_ended_util.get_controller_url() controller_url = open_ended_util.get_controller_url()
controller_qs = ControllerQueryService(controller_url) controller_qs = ControllerQueryService(controller_url)
...@@ -81,66 +76,44 @@ def staff_grading(request, course_id): ...@@ -81,66 +76,44 @@ def staff_grading(request, course_id):
# Checked above # Checked above
'staff_access': True, }) 'staff_access': True, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id): def peer_grading(request, course_id):
''' '''
Show a peer grading interface Show a peer grading interface
''' '''
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_norun = "/".join(course_id_parts[0:2])
pg_location = "i4x://" + course_id_norun + "/peergrading/init"
# call problem list service base_course_url = reverse('courses')
success = False
error_text = ""
problem_list = []
try: try:
problem_list_json = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user)) problem_url_parts = search.path_to_location(modulestore(), course.id, pg_location)
problem_list_dict = json.loads(problem_list_json) problem_url = generate_problem_url(problem_url_parts, base_course_url)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = problem_list_dict['problem_list']
except GradingServiceError:
error_text = "Error occured while contacting the grading service"
success = False
# catch error if if the json loads fails
except ValueError:
error_text = "Could not get problem list"
success = False
ajax_url = _reverse_with_slash('peer_grading', course_id)
return render_to_response('peer_grading/peer_grading.html', { return HttpResponseRedirect(problem_url)
'course': course, except:
'course_id': course_id, error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff."
'ajax_url': ajax_url, log.exception(error_message + "Current course is: {0}".format(course_id))
'success': success, return HttpResponse(error_message)
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) def generate_problem_url(problem_url_parts, base_course_url):
def peer_grading_problem(request, course_id): """
''' From a list of problem url parts generated by search.path_to_location and a base course url, generates a url to a problem
Show individual problem interface @param problem_url_parts: Output of search.path_to_location
''' @param base_course_url: Base url of a given course
course = get_course_with_access(request.user, course_id, 'load') @return: A path to the problem
problem_location = request.GET.get("location") """
problem_url = base_course_url + "/"
ajax_url = _reverse_with_slash('peer_grading', course_id) for z in xrange(0,len(problem_url_parts)):
part = problem_url_parts[z]
if part is not None:
if z==1:
problem_url += "courseware/"
problem_url += part + "/"
return problem_url
return render_to_response('peer_grading/peer_grading_problem.html', {
'view_html': '',
'course': course,
'problem_location': problem_location,
'course_id': course_id,
'ajax_url': ajax_url,
# Checked above
'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def student_problem_list(request, course_id): def student_problem_list(request, course_id):
...@@ -156,28 +129,22 @@ def student_problem_list(request, course_id): ...@@ -156,28 +129,22 @@ def student_problem_list(request, course_id):
problem_list = [] problem_list = []
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: #try:
problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json) problem_list_dict = json.loads(problem_list_json)
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']
problem_list = [] problem_list = []
else: else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
for i in xrange(0,len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
problem_url = base_course_url + "/"
for z in xrange(0,len(problem_url_parts)):
part = problem_url_parts[z]
if part is not None:
if z==1:
problem_url += "courseware/"
problem_url += part + "/"
problem_list[i].update({'actual_url' : problem_url}) for i in xrange(0,len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
problem_url = generate_problem_url(problem_url_parts, base_course_url)
problem_list[i].update({'actual_url' : problem_url})
"""
except GradingServiceError: except GradingServiceError:
error_text = "Error occured while contacting the grading service" error_text = "Error occured while contacting the grading service"
success = False success = False
...@@ -185,6 +152,7 @@ def student_problem_list(request, course_id): ...@@ -185,6 +152,7 @@ def student_problem_list(request, course_id):
except ValueError: except ValueError:
error_text = "Could not get problem list" error_text = "Could not get problem list"
success = False success = False
"""
ajax_url = _reverse_with_slash('open_ended_problems', course_id) ajax_url = _reverse_with_slash('open_ended_problems', course_id)
...@@ -231,16 +199,17 @@ def flagged_problem_list(request, course_id): ...@@ -231,16 +199,17 @@ def flagged_problem_list(request, course_id):
success = False success = False
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
context = {
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', { 'course': course,
'course': course, 'course_id': course_id,
'course_id': course_id, 'ajax_url': ajax_url,
'ajax_url': ajax_url, 'success': success,
'success': success, 'problem_list': problem_list,
'problem_list': problem_list, 'error_text': error_text,
'error_text': error_text, # Checked above
# Checked above 'staff_access': True,
'staff_access': True, }) }
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def combined_notifications(request, course_id): def combined_notifications(request, course_id):
...@@ -322,7 +291,7 @@ def take_action_on_flags(request, course_id): ...@@ -322,7 +291,7 @@ def take_action_on_flags(request, course_id):
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type) response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id)) log.exception("Error saving calibration grade, submission_id: {0}, submission_key: {1}, grader_id: {2}".format(submission_id, submission_key, grader_id))
return _err_response('Could not connect to grading service') return _err_response('Could not connect to grading service')
...@@ -266,24 +266,6 @@ STATICFILES_DIRS = [ ...@@ -266,24 +266,6 @@ STATICFILES_DIRS = [
COMMON_ROOT / "static", COMMON_ROOT / "static",
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
] ]
if os.path.isdir(DATA_DIR):
# Add the full course repo if there is no static directory
STATICFILES_DIRS += [
# TODO (cpennington): When courses are stored in a database, this
# should no longer be added to STATICFILES
(course_dir, DATA_DIR / course_dir)
for course_dir in os.listdir(DATA_DIR)
if (os.path.isdir(DATA_DIR / course_dir) and
not os.path.isdir(DATA_DIR / course_dir / 'static'))
]
# Otherwise, add only the static directory from the course dir
STATICFILES_DIRS += [
# TODO (cpennington): When courses are stored in a database, this
# should no longer be added to STATICFILES
(course_dir, DATA_DIR / course_dir / 'static')
for course_dir in os.listdir(DATA_DIR)
if (os.path.isdir(DATA_DIR / course_dir / 'static'))
]
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
...@@ -437,7 +419,6 @@ main_vendor_js = [ ...@@ -437,7 +419,6 @@ main_vendor_js = [
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/open_ended/**/*.coffee')) open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/open_ended/**/*.coffee'))
PIPELINE_CSS = { PIPELINE_CSS = {
...@@ -469,7 +450,7 @@ PIPELINE_JS = { ...@@ -469,7 +450,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js + open_ended_js) set(courseware_js + discussion_js + staff_grading_js + open_ended_js)
) + [ ) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
...@@ -499,10 +480,6 @@ PIPELINE_JS = { ...@@ -499,10 +480,6 @@ PIPELINE_JS = {
'source_filenames': staff_grading_js, 'source_filenames': staff_grading_js,
'output_filename': 'js/staff_grading.js' 'output_filename': 'js/staff_grading.js'
}, },
'peer_grading' : {
'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js'
},
'open_ended' : { 'open_ended' : {
'source_filenames': open_ended_js, 'source_filenames': open_ended_js,
'output_filename': 'js/open_ended.js' 'output_filename': 'js/open_ended.js'
...@@ -566,6 +543,7 @@ INSTALLED_APPS = ( ...@@ -566,6 +543,7 @@ INSTALLED_APPS = (
# For asset pipelining # For asset pipelining
'pipeline', 'pipeline',
'staticfiles', 'staticfiles',
'static_replace',
# Our courseware # Our courseware
'circuit', 'circuit',
......
...@@ -106,6 +106,27 @@ VIRTUAL_UNIVERSITIES = [] ...@@ -106,6 +106,27 @@ VIRTUAL_UNIVERSITIES = []
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
############################## Course static files ##########################
if os.path.isdir(DATA_DIR):
# Add the full course repo if there is no static directory
STATICFILES_DIRS += [
# TODO (cpennington): When courses are stored in a database, this
# should no longer be added to STATICFILES
(course_dir, DATA_DIR / course_dir)
for course_dir in os.listdir(DATA_DIR)
if (os.path.isdir(DATA_DIR / course_dir) and
not os.path.isdir(DATA_DIR / course_dir / 'static'))
]
# Otherwise, add only the static directory from the course dir
STATICFILES_DIRS += [
# TODO (cpennington): When courses are stored in a database, this
# should no longer be added to STATICFILES
(course_dir, DATA_DIR / course_dir / 'static')
for course_dir in os.listdir(DATA_DIR)
if (os.path.isdir(DATA_DIR / course_dir / 'static'))
]
################################# mitx revision string ##################### ################################# mitx revision string #####################
MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip() MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip()
......
...@@ -120,7 +120,7 @@ div.peer-grading{ ...@@ -120,7 +120,7 @@ div.peer-grading{
margin-right:20px; margin-right:20px;
> div > div
{ {
padding: 10px; padding: 2px;
margin: 0px; margin: 0px;
background: #eee; background: #eee;
height: 10em; height: 10em;
......
<%inherit file="/main.html" /> <section class="container peer-grading-container">
<%block name="bodyclass">${course.css_class}</%block> <div class="peer-grading" data-ajax-url="${ajax_url}" data-use-single-location="${use_single_location}">
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} Peer Grading</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='peer_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}">
<div class="error-container">${error_text}</div> <div class="error-container">${error_text}</div>
<h1>Peer Grading</h1> <h1>Peer Grading</h1>
<h2>Instructions</h2> <h2>Instructions</h2>
...@@ -38,7 +22,7 @@ ...@@ -38,7 +22,7 @@
%for problem in problem_list: %for problem in problem_list:
<tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}"> <tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}">
<td class="problem-name"> <td class="problem-name">
<a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']}</a> <a href="#problem" data-location="${problem['location']}" class="problem-button">${problem['problem_name']}</a>
</td> </td>
<td> <td>
${problem['num_graded']} ${problem['num_graded']}
......
<section class="container peer-grading-container">
<%inherit file="/main.html" /> <div class="peer-grading" data-ajax-url="${ajax_url}" data-location="${problem_location}" data-use-single-location="${use_single_location}">
<%block name="bodyclass">${course.css_class}</%block>
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} Peer Grading.</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='peer_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}" data-location="${problem_location}">
<div class="error-container"></div> <div class="error-container"></div>
<section class="content-panel"> <section class="content-panel">
......
...@@ -268,23 +268,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -268,23 +268,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$',
'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), 'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
# Peer Grading
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/problem$',
'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$',
'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$',
'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/is_student_calibrated$',
'open_ended_grading.peer_grading_service.is_student_calibrated', name='peer_grading_is_student_calibrated'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_grade$',
'open_ended_grading.peer_grading_service.save_grade', name='peer_grading_save_grade'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_calibration_essay$',
'open_ended_grading.peer_grading_service.save_calibration_essay', name='peer_grading_save_calibration_essay'),
# Open Ended problem list # Open Ended problem list
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
'open_ended_grading.views.student_problem_list', name='open_ended_problems'), 'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
...@@ -317,6 +300,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -317,6 +300,9 @@ if settings.COURSEWARE_ENABLED:
# Open Ended Notifications # Open Ended Notifications
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_notifications$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_notifications$',
'open_ended_grading.views.combined_notifications', name='open_ended_notifications'), 'open_ended_grading.views.combined_notifications', name='open_ended_notifications'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
) )
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
......
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