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
[run]
data_file = reports/cms/.coverage
source = cms
source = cms,common/djangoapps
omit = cms/envs/*, cms/manage.py
[report]
......
import logging
from static_replace import replace_urls
from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
......@@ -18,7 +18,17 @@ def get_module_info(store, location, parent_location = None, rewrite_static_link
data = module.definition['data']
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 {
'id': module.location.url(),
......@@ -47,7 +57,7 @@ def set_module_info(store, location, post_data):
if post_data.get('data') is not None:
data = post_data['data']
store.update_item(location, data)
# 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
# deleting the children object from the children collection
......
......@@ -52,10 +52,6 @@ LOGGING = get_logger_config(LOG_DIR,
debug=False,
service_variant=SERVICE_VARIANT)
with open(ENV_ROOT / "repos.json") as repos_file:
REPOS = json.load(repos_file)
################ SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
......
......@@ -285,4 +285,5 @@ INSTALLED_APPS = (
# For asset pipelining
'pipeline',
'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
import json
import logging
import time
import static_replace
from django.conf import settings
from functools import wraps
from static_replace import replace_urls
from mitxmako.shortcuts import render_to_string
from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule
......@@ -49,10 +49,10 @@ def replace_course_urls(get_html, course_id):
"""
@wraps(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
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
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):
@wraps(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
def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem.
Part of staff member debug info.
......
// Generated by CoffeeScript 1.3.3
// Generated by CoffeeScript 1.4.0
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3
// Generated by CoffeeScript 1.4.0
(function() {
var TestProblemGenerator, root,
__hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3
// Generated by CoffeeScript 1.4.0
(function() {
var TestProblemGrader, root,
__hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3
// Generated by CoffeeScript 1.4.0
(function() {
var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
......
......@@ -27,6 +27,7 @@ setup(
"html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
......
......@@ -369,7 +369,7 @@ class CapaModule(XModule):
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
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):
'''
......@@ -490,7 +490,7 @@ class CapaModule(XModule):
new_answers = dict()
for answer_id in answers:
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:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]}
......
......@@ -36,7 +36,9 @@ class CombinedOpenEndedRubric(object):
'max_score': max_score})
success = True
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
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score):
......
......@@ -52,13 +52,17 @@ em, i {
}
strong, b {
font-style: bold;
font-weight: bold;
}
p + p, ul + p, ol + p {
margin-top: 20px;
}
blockquote {
margin: 1em 40px;
}
ol, ul {
margin: 1em 0;
padding: 0 0 0 1em;
......
......@@ -5,16 +5,8 @@ import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
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 lxml import etree
from mitxmako.shortcuts import render_to_string
from xmodule.x_module import ModuleSystem
log = logging.getLogger(__name__)
......@@ -31,7 +23,7 @@ class GradingService(object):
self.url = config['url']
self.login_url = self.url + '/login/'
self.session = requests.session()
self.system = ModuleSystem(None, None, None, render_to_string, None)
self.system = config['system']
def _login(self):
"""
......@@ -42,20 +34,20 @@ class GradingService(object):
Returns the decoded json dict of the response.
"""
response = self.session.post(self.login_url,
{'username': self.username,
'password': self.password,})
{'username': self.username,
'password': self.password,})
response.raise_for_status()
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
"""
try:
op = lambda: self.session.post(url, data=data,
allow_redirects=allow_redirects)
allow_redirects=allow_redirects)
r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace.
......@@ -69,8 +61,8 @@ class GradingService(object):
"""
log.debug(params)
op = lambda: self.session.get(url,
allow_redirects=allow_redirects,
params=params)
allow_redirects=allow_redirects,
params=params)
try:
r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err:
......@@ -78,7 +70,7 @@ class GradingService(object):
raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text
def _try_with_login(self, operation):
"""
......@@ -96,8 +88,8 @@ class GradingService(object):
r = self._login()
if r and not r.get('success'):
log.warning("Couldn't log into staff_grading backend. Response: %s",
r)
# try again
r)
# try again
response = operation()
response.raise_for_status()
......@@ -113,23 +105,23 @@ class GradingService(object):
"""
try:
response_json = json.loads(response)
except:
response_json = response
try:
if 'rubric' in response_json:
rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(self.system, view_only)
success, rubric_html = rubric_renderer.render_rubric(rubric)
response_json['rubric'] = rubric_html
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:
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
.format(rubric))
return {'success': False,
'error': 'Error displaying submission'}
'error': 'Error displaying submission'}
except ValueError:
log.exception("Error parsing response: {0}".format(response))
return {'success': False,
'error': "Error displaying submission"}
'error': "Error displaying submission"}
......@@ -10,7 +10,8 @@ class @HTMLEditingDescriptor
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
# instances (like sandbox). It is not necessary to explicitly set baseURL when running locally.
......@@ -43,16 +44,21 @@ class @HTMLEditingDescriptor
theme_advanced_blockformats : "p,pre,h1,h2,h3",
width: '100%',
height: '400px',
setup : HTMLEditingDescriptor.setupTinyMCE,
setup : @setupTinyMCE,
# Cannot get access to tinyMCE Editor instance (for focusing) until after it is rendered.
# The tinyMCE callback passes in the editor as a paramter.
init_instance_callback: @focusVisualEditor
})
@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', {
title : 'Code',
image : '/static/images/ico-tinymce-code.png',
......@@ -67,22 +73,23 @@ class @HTMLEditingDescriptor
command.setActive('wrapAsCode', e.nodeName == 'CODE')
)
onSwitchEditor: (e)=>
e.preventDefault();
@visualEditor = ed
if not $(e.currentTarget).hasClass('current')
element = e.data.element
onSwitchEditor: (e) =>
e.preventDefault();
$(e.currentTarget).addClass('current')
$(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass)
$(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass)
$currentTarget = $(e.currentTarget)
if not $currentTarget.hasClass('current')
$currentTarget.addClass('current')
@$mceToolbar.toggleClass(HTMLEditingDescriptor.isInactiveClass)
@$advancedEditorWrapper.toggleClass(HTMLEditingDescriptor.isInactiveClass)
visualEditor = @getVisualEditor(element)
if $(e.currentTarget).attr('data-tab') is 'visual'
$(element).find('.html-tab').removeClass('current')
visualEditor = @getVisualEditor()
if $currentTarget.data('tab') is 'visual'
@$htmlTab.removeClass('current')
@showVisualEditor(visualEditor)
else
$(element).find('.visual-tab').removeClass('current')
@$visualTab.removeClass('current')
@showAdvancedEditor(visualEditor)
# Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
......@@ -104,20 +111,24 @@ class @HTMLEditingDescriptor
@focusVisualEditor(visualEditor)
@showingVisualEditor = true
focusVisualEditor: (visualEditor) ->
focusVisualEditor: (visualEditor) =>
visualEditor.focus()
if not @$mceToolbar?
@$mceToolbar = $(@element).find('table.mceToolbar')
getVisualEditor: (element) ->
getVisualEditor: () ->
###
Returns the instance of TinyMCE.
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: ->
@element.off('click', '.editor-tabs .tab', @onSwitchEditor)
text = @advanced_editor.getValue()
visualEditor = @getVisualEditor(@element)
visualEditor = @getVisualEditor()
if @showingVisualEditor and visualEditor.isDirty()
text = visualEditor.getContent({no_events: 1})
data: text
......@@ -2,17 +2,27 @@
# and message container when they are empty
# Can (and should be) expanded upon when our problem list
# becomes more sophisticated
class PeerGrading
constructor: () ->
class @PeerGrading
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.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@problem_list = $('.problem-list')
@construct_progress_bar()
if @use_single_location
@activate_problem()
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()
problems.each( (index, element) =>
......@@ -22,6 +32,18 @@ class PeerGrading
bar_max = parseInt(problem.data('required')) + bar_value
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
......@@ -13,6 +13,10 @@ from urlparse import urlparse
import requests
from boto.s3.connection import S3Connection
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
import pickle
import logging
......
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
from xmodule.x_module import XModule
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.contentstore.content import StaticContent
......@@ -121,12 +121,12 @@ class VideoModule(XModule):
return self.youtube
def get_html(self):
if isinstance(modulestore(), MongoModuleStore) :
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
if isinstance(modulestore(), XMLModuleStore) :
# VS[compat]
# cdodge: filesystem static content support.
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', {
'streams': self.video_list(),
......
<peergrading display_name = "Peer Grading" use_for_single_location="False" is_graded="False"/>
# .coveragerc for lms
[run]
data_file = reports/lms/.coverage
source = lms
source = lms,common/djangoapps
omit = lms/envs/*
[report]
......
......@@ -19,7 +19,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore.exceptions import ItemNotFoundError
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
import branding
from courseware.models import StudentModuleCache
......@@ -83,13 +83,12 @@ def get_opt_course_with_access(user, course_id, action):
return None
return get_course_with_access(user, course_id, action)
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if isinstance(modulestore(), XMLModuleStore):
path = course.metadata['data_dir'] + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
return '/static/' + course.metadata['data_dir'] + "/images/course_image.jpg"
else:
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
path = StaticContent.get_url_path_from_location(loc)
......@@ -224,8 +223,11 @@ def get_course_syllabus_section(course, section_key):
dirs = [path("syllabus") / course.url_name, path("syllabus")]
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'], course_namespace=course.location)
return replace_static_urls(
htmlFile.read().decode('utf-8'),
course.metadata['data_dir'],
course_namespace=course.location
)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url()))
......
......@@ -2,6 +2,9 @@ import json
import logging
import pyparsing
import sys
import static_replace
from functools import partial
from django.conf import settings
from django.contrib.auth.models import User
......@@ -18,7 +21,6 @@ from courseware.access import has_access
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from static_replace import replace_urls
from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError
......@@ -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
# a module is coming through get_html and is therefore covered
# 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,
anonymous_student_id=unique_id_for_user(user),
course_id=course_id,
......@@ -280,7 +286,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
module.get_html = replace_static_urls(
_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))
# Allow URLs of the form '/course/' refer to the root of multicourse directory
......
......@@ -19,12 +19,10 @@ from django.core.urlresolvers import reverse
from fs.errors import ResourceNotFoundError
from courseware.access import has_access
from static_replace import replace_urls
from lxml.html import rewrite_links
from module_render import get_module
from courseware.access import has_access
from static_replace import replace_urls
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
......@@ -322,4 +320,4 @@ def get_static_tab_contents(request, cache, course, tab):
if tab_module is not None:
html = tab_module.get_html()
return html
\ No newline at end of file
return html
......@@ -12,6 +12,9 @@ import pystache_custom as pystache
import urllib
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):
if int(count) >= 2 or int(count) == 0:
return singular_term + 's'
......
......@@ -46,11 +46,13 @@ class Role(models.Model):
def add_permission(self, permission):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission):
course = get_course_by_id(self.course_id)
if self.name == FORUM_ROLE_STUDENT and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
changing_comments = permission.startswith('edit') or \
permission.startswith('update') or permission.startswith('create')
in_blackout_period = not course.forum_posts_allowed
if (self.name == FORUM_ROLE_STUDENT) and in_blackout_period and changing_comments:
return False
return self.permissions.filter(name=permission).exists()
......
......@@ -5,6 +5,8 @@ import urllib
import sys
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):
num, word = text.split(' ')
num = int(num or '0')
......
......@@ -29,6 +29,7 @@ def has_permission(user, permission, course_id=None):
CONDITIONS = ['is_open', 'is_author']
# data may be a json file
def check_condition(user, condition, course_id, data):
def check_open(user, condition, course_id, data):
try:
......@@ -61,8 +62,10 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
def test(user, per, operator="or"):
if isinstance(per, basestring):
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)
# 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"]:
results = [test(user, x, operator="and") for x in per]
if operator == "or":
......@@ -102,4 +105,4 @@ def check_permissions_by_view(user, course_id, content, name):
p = VIEW_PERMISSIONS[name]
except KeyError:
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):
return isinstance(v, str) and len(v.strip()) == 0
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):
return dict(dic1.items() + dic2.items())
......
......@@ -3,11 +3,12 @@ import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
from grading_service import GradingService
from grading_service import GradingServiceError
from xmodule.grading_service_module import GradingService, GradingServiceError
from django.conf import settings
from django.http import HttpResponse, Http404
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__)
......@@ -16,6 +17,7 @@ class ControllerQueryService(GradingService):
Interface to staff grading backend.
"""
def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(ControllerQueryService, self).__init__(config)
self.check_eta_url = self.url + '/get_submission_eta/'
self.is_unique_url = self.url + '/is_name_unique/'
......
from django.conf import settings
from staff_grading_service import StaffGradingService
from peer_grading_service import PeerGradingService
from open_ended_grading.controller_query_service import ControllerQueryService
import json
from student.models import unique_id_for_user
......@@ -10,6 +9,7 @@ import logging
from courseware.access import has_access
from util.cache import cache
import datetime
from xmodule import peer_grading_service
log=logging.getLogger(__name__)
......
......@@ -7,8 +7,7 @@ import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
from grading_service import GradingService
from grading_service import GradingServiceError
from xmodule.grading_service_module import GradingService, GradingServiceError
from django.conf import settings
from django.http import HttpResponse, Http404
......@@ -22,8 +21,6 @@ from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__)
class MockStaffGradingService(object):
"""
A simple mockup of a staff grading service, testing.
......@@ -64,6 +61,7 @@ class StaffGradingService(GradingService):
Interface to staff grading backend.
"""
def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(StaffGradingService, self).__init__(config)
self.get_next_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/'
......
......@@ -6,7 +6,7 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open
from django.test import TestCase
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.contrib.auth.models import Group
......@@ -17,10 +17,13 @@ import xmodule.modulestore.django
from nose import SkipTest
from mock import patch, Mock
import json
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
import logging
log = logging.getLogger(__name__)
from override_settings import override_settings
from django.http import QueryDict
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
......@@ -98,6 +101,7 @@ class TestStaffGradingService(ct.PageLoader):
'submission_id': '123',
'location': self.location,
'rubric_scores[]': ['1', '2']}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success'], str(d))
......@@ -136,19 +140,21 @@ class TestPeerGradingService(ct.PageLoader):
self.course_id = "edX/toy/2012_Fall"
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()
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}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.get_next_submission(data)
d = json.loads(r)
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
......@@ -156,63 +162,48 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score'])
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 = {}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.get_next_submission(data)
d = r
self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location")
def test_save_grade_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_grade', kwargs={'course_id': self.course_id})
data = {'location': self.location,
'submission_id': '1',
'submission_key': 'fake key',
'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)
raise SkipTest()
data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
qdict = QueryDict(data.replace("|","&"))
r = self.peer_module.save_grade(qdict)
d = r
self.assertTrue(d['success'])
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 = {}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.save_grade(data)
d = r
self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1)
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}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.is_student_calibrated(data)
d = json.loads(r)
self.assertTrue(d['success'])
self.assertTrue('calibrated' in d)
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 = {}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.is_student_calibrated(data)
d = r
self.assertFalse(d['success'])
self.assertFalse('calibrated' in d)
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}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.show_calibration_essay(data)
d = json.loads(r)
log.debug(d)
log.debug(type(d))
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
......@@ -220,37 +211,27 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score'])
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 = {}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.show_calibration_essay(data)
d = r
self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location")
def test_save_calibration_essay_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_calibration_essay', kwargs={'course_id': self.course_id})
data = {'location': self.location,
'submission_id': '1',
'submission_key': 'fake key',
'score': '2',
'feedback': 'This is feedback',
'rubric_scores[]': [1, 2]}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
raise SkipTest()
data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
qdict = QueryDict(data.replace("|","&"))
r = self.peer_module.save_calibration_essay(qdict)
d = r
self.assertTrue(d['success'])
self.assertTrue('actual_score' in d)
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 = {}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
r = self.peer_module.save_calibration_essay(data)
d = r
self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1)
self.assertFalse('actual_score' in d)
......
......@@ -2,6 +2,7 @@
import logging
import urllib
import re
from django.conf import settings
from django.views.decorators.cache import cache_control
......@@ -11,10 +12,8 @@ from django.core.urlresolvers import reverse
from student.models import unique_id_for_user
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 grading_service import GradingServiceError
from xmodule.grading_service_module import GradingServiceError
import json
from .staff_grading import StaffGrading
from student.models import unique_id_for_user
......@@ -25,15 +24,11 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search
from django.http import HttpResponse, Http404
from django.http import HttpResponse, Http404, HttpResponseRedirect
log = logging.getLogger(__name__)
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_qs = ControllerQueryService(controller_url)
......@@ -81,66 +76,44 @@ def staff_grading(request, course_id):
# Checked above
'staff_access': True, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id):
'''
Show a peer grading interface
'''
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
success = False
error_text = ""
problem_list = []
base_course_url = reverse('courses')
try:
problem_list_json = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(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 = _reverse_with_slash('peer_grading', course_id)
problem_url_parts = search.path_to_location(modulestore(), course.id, pg_location)
problem_url = generate_problem_url(problem_url_parts, base_course_url)
return render_to_response('peer_grading/peer_grading.html', {
'course': course,
'course_id': course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': False, })
return HttpResponseRedirect(problem_url)
except:
error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff."
log.exception(error_message + "Current course is: {0}".format(course_id))
return HttpResponse(error_message)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading_problem(request, course_id):
'''
Show individual problem interface
'''
course = get_course_with_access(request.user, course_id, 'load')
problem_location = request.GET.get("location")
ajax_url = _reverse_with_slash('peer_grading', course_id)
def generate_problem_url(problem_url_parts, base_course_url):
"""
From a list of problem url parts generated by search.path_to_location and a base course url, generates a url to a problem
@param problem_url_parts: Output of search.path_to_location
@param base_course_url: Base url of a given course
@return: A path to the problem
"""
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 + "/"
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)
def student_problem_list(request, course_id):
......@@ -156,28 +129,22 @@ def student_problem_list(request, course_id):
problem_list = []
base_course_url = reverse('courses')
try:
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)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = []
else:
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 + "/"
#try:
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)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = []
else:
problem_list = problem_list_dict['problem_list']
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:
error_text = "Error occured while contacting the grading service"
success = False
......@@ -185,6 +152,7 @@ def student_problem_list(request, course_id):
except ValueError:
error_text = "Could not get problem list"
success = False
"""
ajax_url = _reverse_with_slash('open_ended_problems', course_id)
......@@ -231,16 +199,17 @@ def flagged_problem_list(request, course_id):
success = False
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', {
'course': course,
'course_id': course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': True, })
context = {
'course': course,
'course_id': course_id,
'ajax_url': ajax_url,
'success': success,
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'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)
def combined_notifications(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)
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))
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')
......@@ -266,24 +266,6 @@ STATICFILES_DIRS = [
COMMON_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
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
......@@ -437,7 +419,6 @@ main_vendor_js = [
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'))
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'))
PIPELINE_CSS = {
......@@ -469,7 +450,7 @@ PIPELINE_JS = {
'source_filenames': sorted(
set(rooted_glob(COMMON_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/my_courses_dropdown.js',
......@@ -499,10 +480,6 @@ PIPELINE_JS = {
'source_filenames': staff_grading_js,
'output_filename': 'js/staff_grading.js'
},
'peer_grading' : {
'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js'
},
'open_ended' : {
'source_filenames': open_ended_js,
'output_filename': 'js/open_ended.js'
......@@ -566,6 +543,7 @@ INSTALLED_APPS = (
# For asset pipelining
'pipeline',
'staticfiles',
'static_replace',
# Our courseware
'circuit',
......
......@@ -106,6 +106,27 @@ VIRTUAL_UNIVERSITIES = []
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_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip()
......
......@@ -120,7 +120,7 @@ div.peer-grading{
margin-right:20px;
> div
{
padding: 10px;
padding: 2px;
margin: 0px;
background: #eee;
height: 10em;
......
<%inherit file="/main.html" />
<%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}">
<section class="container peer-grading-container">
<div class="peer-grading" data-ajax-url="${ajax_url}" data-use-single-location="${use_single_location}">
<div class="error-container">${error_text}</div>
<h1>Peer Grading</h1>
<h2>Instructions</h2>
......@@ -38,7 +22,7 @@
%for problem in problem_list:
<tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}">
<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>
${problem['num_graded']}
......
<%inherit file="/main.html" />
<%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}">
<section class="container peer-grading-container">
<div class="peer-grading" data-ajax-url="${ajax_url}" data-location="${problem_location}" data-use-single-location="${use_single_location}">
<div class="error-container"></div>
<section class="content-panel">
......
......@@ -268,23 +268,6 @@ if settings.COURSEWARE_ENABLED:
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'),
# 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
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
......@@ -317,6 +300,9 @@ if settings.COURSEWARE_ENABLED:
# Open Ended Notifications
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/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
......
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