Commit 40854fc1 by Diana Huang

Merge branch 'master' into feature/diana/close-oe-problems

parents a7588410 95d39573
# .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
......
......@@ -515,6 +515,9 @@ class ContentStoreTest(TestCase):
# note, we know the link it should be because that's what in the 'full' course in the test data
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_missing_static_content(self):
resp = self.client.get("/c4x/asd/asd/asd/asd")
self.assertEqual(resp.status_code, 404)
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
......
......@@ -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',
)
......@@ -21,7 +21,9 @@ class StaticContentServer(object):
try:
content = contentstore().find(loc)
except NotFoundError:
raise Http404
response = HttpResponse()
response.status_code = 404
return response
# since we fetched it from DB, let's cache it going forward
set_cached_content(content)
......
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]}
......
......@@ -33,7 +33,9 @@ class CombinedOpenEndedRubric(object):
'view_only': self.view_only})
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):
......
......@@ -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, False)
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"}
\ No newline at end of file
......@@ -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"/>
......@@ -268,6 +268,7 @@ Supported fields at the course level:
* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00".
* "advertised_start" -- specify what you want displayed as the start date of the course in the course listing and course about pages. This can be useful if you want to let people in early before the formal start. Format-by-example: "2012-09-05T12:00".
* "disable_policy_graph" -- set to true (or "Yes"), if the policy graph should be disabled (ie not shown).
* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start".
* "end" -- specify the end date for the course. Format-by-example: "2012-11-05T12:00".
* "end_of_course_survey_url" -- a url for an end of course survey -- shown after course is over, next to certificate download links.
......
# .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'
......
......@@ -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')
......
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 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
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
from xmodule import peer_grading_service
import json
from student.models import unique_id_for_user
import open_ended_util
......@@ -10,6 +10,9 @@ import logging
from courseware.access import has_access
from util.cache import cache
import datetime
from xmodule import peer_grading_service
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
log=logging.getLogger(__name__)
......@@ -55,7 +58,8 @@ def staff_grading_notifications(course, user):
return notification_dict
def peer_grading_notifications(course, user):
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
system = ModuleSystem(None,None,None,render_to_string,None)
peer_gs = peer_grading_service.PeerGradingService(settings.PEER_GRADING_INTERFACE, system)
pending_grading=False
img_path= ""
course_id = course.id
......
......@@ -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'
......@@ -535,7 +512,7 @@ PIPELINE_COMPILERS = [
'pipeline.compilers.coffee.CoffeeScriptCompiler',
]
PIPELINE_SASS_ARGUMENTS = '-t expanded -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
PIPELINE_CSS_COMPRESSOR = None
PIPELINE_JS_COMPRESSOR = None
......@@ -545,7 +522,7 @@ STATICFILES_IGNORE_PATTERNS = (
"coffee/*",
)
# PIPELINE_YUI_BINARY = 'yui-compressor'
PIPELINE_YUI_BINARY = 'yui-compressor'
PIPELINE_SASS_BINARY = 'sass'
PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee'
......@@ -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;
......
......@@ -32,7 +32,9 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
<h1>Course Progress</h1>
</header>
<div id="grade-detail-graph"></div>
%if not course.metadata.get('disable_progress_graph',False):
<div id="grade-detail-graph"></div>
%endif
<ol class="chapters">
%for chapter in courseware_summary:
......
<%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