Merge pull request #1269 from MITx/feature/cdodge/cms-master-merge3
Feature/cdodge/cms master merge3
This commit is contained in:
@@ -202,7 +202,7 @@ def edit_subsection(request, location):
|
||||
if item.location.category != 'sequential':
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
parent_locs = modulestore().get_parent_locations(location)
|
||||
parent_locs = modulestore().get_parent_locations(location, None)
|
||||
|
||||
# we're for now assuming a single parent
|
||||
if len(parent_locs) != 1:
|
||||
@@ -285,10 +285,10 @@ def edit_unit(request, location):
|
||||
# this will need to change to check permissions correctly so as
|
||||
# to pick the correct parent subsection
|
||||
|
||||
containing_subsection_locs = modulestore().get_parent_locations(location)
|
||||
containing_subsection_locs = modulestore().get_parent_locations(location, None)
|
||||
containing_subsection = modulestore().get_item(containing_subsection_locs[0])
|
||||
|
||||
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location)
|
||||
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
|
||||
containing_section = modulestore().get_item(containing_section_locs[0])
|
||||
|
||||
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
|
||||
|
||||
@@ -78,7 +78,7 @@ def index(request, extra_context={}, user=None):
|
||||
courses = get_courses(None, domain=domain)
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
# Get the 3 most recent news
|
||||
|
||||
@@ -633,7 +633,7 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
|
||||
self.correct_choices = [choice.get('name') for choice in cxml]
|
||||
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
@@ -727,7 +727,7 @@ class OptionResponse(LoncapaResponse):
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'), af.get('correct')) for af in self.answer_fields])
|
||||
amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields])
|
||||
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
|
||||
return amap
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import logging
|
||||
from cStringIO import StringIO
|
||||
from lxml import etree
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
from xmodule.graders import grader_from_conf
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time, stringify_time
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import grader_from_conf
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
@@ -16,11 +21,13 @@ import copy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True)
|
||||
|
||||
_cached_toc = {}
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
@@ -334,6 +341,38 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def show_calculator(self):
|
||||
return self.metadata.get("show_calculator", None) == "Yes"
|
||||
|
||||
@property
|
||||
def is_new(self):
|
||||
# The course is "new" if either if the metadata flag is_new is
|
||||
# true or if the course has not started yet
|
||||
flag = self.metadata.get('is_new', None)
|
||||
if flag is None:
|
||||
return self.days_until_start > 1
|
||||
elif isinstance(flag, basestring):
|
||||
return flag.lower() in ['true', 'yes', 'y']
|
||||
else:
|
||||
return bool(flag)
|
||||
|
||||
@property
|
||||
def days_until_start(self):
|
||||
def convert_to_datetime(timestamp):
|
||||
return datetime.fromtimestamp(time.mktime(timestamp))
|
||||
|
||||
start_date = convert_to_datetime(self.start)
|
||||
|
||||
# Try to use course advertised date if we can parse it
|
||||
advertised_start = self.metadata.get('advertised_start', None)
|
||||
if advertised_start:
|
||||
try:
|
||||
start_date = datetime.strptime(advertised_start,
|
||||
"%Y-%m-%dT%H:%M")
|
||||
except ValueError:
|
||||
pass # Invalid date, keep using 'start''
|
||||
|
||||
now = convert_to_datetime(time.gmtime())
|
||||
days_until_start = (start_date - now).days
|
||||
return days_until_start
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
"""
|
||||
@@ -413,7 +452,6 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
raise ValueError("{0} is not a course location".format(loc))
|
||||
return "/".join([loc.org, loc.course, loc.name])
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Return the course_id for this course"""
|
||||
@@ -427,7 +465,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# form text...
|
||||
if parsed_advertised_start is None and \
|
||||
('advertised_start' in self.metadata):
|
||||
return self.metadata['advertised_start']
|
||||
return self.metadata['advertised_start']
|
||||
|
||||
displayed_start = parsed_advertised_start or self.start
|
||||
|
||||
@@ -511,5 +549,3 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -364,9 +364,9 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
|
||||
@@ -160,13 +160,13 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
return super(DraftModuleStore, self).delete_item(as_draft(location))
|
||||
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
return super(DraftModuleStore, self).get_parent_locations(location)
|
||||
return super(DraftModuleStore, self).get_parent_locations(location, course_id)
|
||||
|
||||
def publish(self, location, published_by_id):
|
||||
"""
|
||||
|
||||
@@ -402,6 +402,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
|
||||
|
||||
def delete_item(self, location):
|
||||
"""
|
||||
Delete an item from this modulestore
|
||||
@@ -420,12 +421,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self.collection.remove({'_id': Location(location).dict()})
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
items = self.collection.find({'definition.children': location.url()},
|
||||
|
||||
@@ -60,9 +60,11 @@ def path_to_location(modulestore, course_id, location):
|
||||
(loc, path) = queue.pop() # Takes from the end
|
||||
loc = Location(loc)
|
||||
|
||||
# Call get_parent_locations first to make sure the location is there
|
||||
# (even if it's a course, and we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc)
|
||||
# get_parent_locations should raise ItemNotFoundError if location
|
||||
# isn't found so we don't have to do it explicitly. Call this
|
||||
# first to make sure the location is there (even if it's a course, and
|
||||
# we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc, course_id)
|
||||
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
if loc.category == "course":
|
||||
|
||||
@@ -23,12 +23,3 @@ def check_path_to_location(modulestore):
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
|
||||
|
||||
# Since our test files are valid, there shouldn't be any
|
||||
# elements with no path to them. But we can look for them in
|
||||
# another course.
|
||||
no_path = (
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, path_to_location, modulestore, course_id, location)
|
||||
|
||||
|
||||
@@ -280,14 +280,16 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
self.parent_tracker = ParentTracker()
|
||||
self.parent_trackers = defaultdict(ParentTracker)
|
||||
|
||||
# If we are specifically asked for missing courses, that should
|
||||
# be an error. If we are asked for "all" courses, find the ones
|
||||
# that have a course.xml
|
||||
# that have a course.xml. We sort the dirs in alpha order so we always
|
||||
# read things in the same order (OS differences in load order have
|
||||
# bitten us in the past.)
|
||||
if course_dirs is None:
|
||||
course_dirs = [d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")]
|
||||
course_dirs = sorted([d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")])
|
||||
|
||||
for course_dir in course_dirs:
|
||||
self.try_load_course(course_dir)
|
||||
@@ -312,7 +314,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
if course_descriptor is not None:
|
||||
self.courses[course_dir] = course_descriptor
|
||||
self._location_errors[course_descriptor.location] = errorlog
|
||||
self.parent_tracker.make_known(course_descriptor.location)
|
||||
self.parent_trackers[course_descriptor.id].make_known(course_descriptor.location)
|
||||
else:
|
||||
# Didn't load course. Instead, save the errors elsewhere.
|
||||
self.errored_courses[course_dir] = errorlog
|
||||
@@ -415,7 +417,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
course_dir,
|
||||
policy,
|
||||
tracker,
|
||||
self.parent_tracker,
|
||||
self.parent_trackers[course_id],
|
||||
self.load_error_modules,
|
||||
)
|
||||
|
||||
@@ -578,12 +580,15 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
return self.parent_tracker.parents(location)
|
||||
if not self.parent_trackers[course_id].is_known(location):
|
||||
raise ItemNotFoundError("{0} not in {1}".format(location, course_id))
|
||||
|
||||
return self.parent_trackers[course_id].parents(location)
|
||||
|
||||
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
from time import strptime, gmtime
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00')
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
|
||||
def __init__(self, load_error_modules):
|
||||
|
||||
xmlstore = XMLModuleStore("data_dir", course_dirs=[],
|
||||
load_error_modules=load_error_modules)
|
||||
course_id = "/".join([ORG, COURSE, 'test_run'])
|
||||
course_dir = "test_dir"
|
||||
policy = {}
|
||||
error_tracker = Mock()
|
||||
parent_tracker = Mock()
|
||||
|
||||
super(DummySystem, self).__init__(
|
||||
xmlstore,
|
||||
course_id,
|
||||
course_dir,
|
||||
policy,
|
||||
error_tracker,
|
||||
parent_tracker,
|
||||
load_error_modules=load_error_modules,
|
||||
)
|
||||
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
@staticmethod
|
||||
def get_dummy_course(start, is_new=None, load_error_modules=True):
|
||||
"""Get a dummy course"""
|
||||
|
||||
system = DummySystem(load_error_modules)
|
||||
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
|
||||
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}"
|
||||
{is_new}>
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new)
|
||||
|
||||
return system.process_xml(start_xml)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_non_started_yet(self, gmtime_mock):
|
||||
descriptor = self.get_dummy_course(start='2013-01-05T12:00')
|
||||
gmtime_mock.return_value = NOW
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start == 4)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_already_started(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00')
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_is_new_set(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
@@ -23,7 +23,13 @@ Install the following:
|
||||
|
||||
### Databases
|
||||
|
||||
Run the following to setup the relational database before starting servers:
|
||||
First start up the mongo daemon. E.g. to start it up in the background
|
||||
using a config file:
|
||||
|
||||
mongod --config /usr/local/etc/mongod.conf &
|
||||
|
||||
Check out the course data directories that you want to work with into the
|
||||
`GITHUB_REPO_ROOT` (by default, `../data`). Then run the following command:
|
||||
|
||||
rake resetdb
|
||||
|
||||
@@ -57,8 +63,12 @@ This runs all the tests (long, uses collectstatic):
|
||||
|
||||
If if you aren't changing static files, can run `rake test` once, then run
|
||||
|
||||
rake fasttest_{lms,cms}
|
||||
rake fasttest_lms
|
||||
|
||||
or
|
||||
|
||||
rake fasttest_cms
|
||||
|
||||
xmodule can be tested independently, with this:
|
||||
|
||||
rake test_common/lib/xmodule
|
||||
|
||||
@@ -255,35 +255,5 @@ def get_courses(user, domain=None):
|
||||
courses = branding.get_visible_courses(domain)
|
||||
courses = [c for c in courses if has_access(user, c, 'see_exists')]
|
||||
|
||||
# Add metadata about the start day and if the course is new
|
||||
for course in courses:
|
||||
days_to_start = _get_course_days_to_start(course)
|
||||
|
||||
metadata = course.metadata
|
||||
metadata['days_to_start'] = days_to_start
|
||||
metadata['is_new'] = course.metadata.get('is_new', days_to_start > 1)
|
||||
|
||||
courses = sorted(courses, key=lambda course:course.number)
|
||||
return courses
|
||||
|
||||
|
||||
def _get_course_days_to_start(course):
|
||||
from datetime import datetime as dt
|
||||
from time import mktime, gmtime
|
||||
|
||||
convert_to_datetime = lambda ts: dt.fromtimestamp(mktime(ts))
|
||||
|
||||
start_date = convert_to_datetime(course.start)
|
||||
|
||||
# If the course has a valid advertised date, use that instead
|
||||
advertised_start = course.metadata.get('advertised_start', None)
|
||||
if advertised_start:
|
||||
try:
|
||||
start_date = dt.strptime(advertised_start, "%Y-%m-%dT%H:%M")
|
||||
except ValueError:
|
||||
pass # Invalid date, keep using course.start
|
||||
|
||||
now = convert_to_datetime(gmtime())
|
||||
days_to_start = (start_date - now).days
|
||||
|
||||
return days_to_start
|
||||
|
||||
@@ -70,7 +70,7 @@ def courses(request):
|
||||
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST'))
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
return render_to_response("courseware/courses.html", {'courses': courses})
|
||||
@@ -439,7 +439,7 @@ def university_profile(request, org_id):
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.metadata['days_to_start']
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
|
||||
@@ -25,7 +25,6 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.utils import has_forum_access
|
||||
|
||||
from instructor import staff_grading_service
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -101,7 +100,6 @@ def action_name(operation, rolename):
|
||||
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
||||
|
||||
|
||||
_mock_service = staff_grading_service.MockStaffGradingService()
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
@@ -224,94 +222,3 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles))>=0, 'not finding roles "{0}"'.format(roles))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffGradingService(ct.PageLoader):
|
||||
'''
|
||||
Check that staff grading service proxy works. Basically just checking the
|
||||
access control and error handling logic -- all the actual work is on the
|
||||
backend.
|
||||
'''
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
self.student = 'view@test.com'
|
||||
self.instructor = 'view2@test.com'
|
||||
self.password = 'foo'
|
||||
self.location = 'TestLocation'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.create_account('u2', self.instructor, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
self.mock_service = staff_grading_service.grading_service()
|
||||
|
||||
self.logout()
|
||||
|
||||
def test_access(self):
|
||||
"""
|
||||
Make sure only staff have access.
|
||||
"""
|
||||
self.login(self.student, self.password)
|
||||
|
||||
# both get and post should return 404
|
||||
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
||||
url = reverse(view_name, kwargs={'course_id': self.course_id})
|
||||
self.check_for_get_code(404, url)
|
||||
self.check_for_post_code(404, url)
|
||||
|
||||
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
||||
data = {'location': self.location}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'])
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
self.assertIsNotNone(d['submission'])
|
||||
self.assertIsNotNone(d['num_graded'])
|
||||
self.assertIsNotNone(d['min_for_ml'])
|
||||
self.assertIsNotNone(d['num_pending'])
|
||||
self.assertIsNotNone(d['prompt'])
|
||||
self.assertIsNotNone(d['ml_error_info'])
|
||||
self.assertIsNotNone(d['max_score'])
|
||||
self.assertIsNotNone(d['rubric'])
|
||||
|
||||
|
||||
def test_save_grade(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
|
||||
data = {'score': '12',
|
||||
'feedback': 'great!',
|
||||
'submission_id': '123',
|
||||
'location': self.location}
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
|
||||
def test_get_problem_list(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
|
||||
data = {}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertIsNotNone(d['problem_list'])
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
import track.views
|
||||
|
||||
from .grading import StaffGrading
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -414,26 +413,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def staff_grading(request, course_id):
|
||||
"""
|
||||
Show the instructor grading interface.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
grading = StaffGrading(course)
|
||||
|
||||
ajax_url = reverse('staff_grading', kwargs={'course_id': course_id})
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
|
||||
return render_to_response('instructor/staff_grading.html', {
|
||||
'view_html': grading.get_html(),
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
# Checked above
|
||||
'staff_access': True, })
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
|
||||
0
lms/djangoapps/open_ended_grading/__init__.py
Normal file
0
lms/djangoapps/open_ended_grading/__init__.py
Normal file
99
lms/djangoapps/open_ended_grading/grading_service.py
Normal file
99
lms/djangoapps/open_ended_grading/grading_service.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# This class gives a common interface for logging into the grading controller
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException, ConnectionError, HTTPError
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, Http404
|
||||
|
||||
from courseware.access import has_access
|
||||
from util.json_request import expect_json
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
class GradingService(object):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
self.url = config['url']
|
||||
self.login_url = self.url + '/login/'
|
||||
self.session = requests.session()
|
||||
|
||||
def _login(self):
|
||||
"""
|
||||
Log into the staff grading service.
|
||||
|
||||
Raises requests.exceptions.HTTPError if something goes wrong.
|
||||
|
||||
Returns the decoded json dict of the response.
|
||||
"""
|
||||
response = self.session.post(self.login_url,
|
||||
{'username': self.username,
|
||||
'password': self.password,})
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json
|
||||
|
||||
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)
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
def get(self, url, params, allow_redirects=False):
|
||||
"""
|
||||
Make a get request to the grading controller
|
||||
"""
|
||||
op = lambda: self.session.get(url,
|
||||
allow_redirects=allow_redirects,
|
||||
params=params)
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
|
||||
def _try_with_login(self, operation):
|
||||
"""
|
||||
Call operation(), which should return a requests response object. If
|
||||
the request fails with a 'login_required' error, call _login() and try
|
||||
the operation again.
|
||||
|
||||
Returns the result of operation(). Does not catch exceptions.
|
||||
"""
|
||||
response = operation()
|
||||
if (response.json
|
||||
and response.json.get('success') == False
|
||||
and response.json.get('error') == 'login_required'):
|
||||
# apparrently we aren't logged in. Try to fix that.
|
||||
r = self._login()
|
||||
if r and not r.get('success'):
|
||||
log.warning("Couldn't log into staff_grading backend. Response: %s",
|
||||
r)
|
||||
# try again
|
||||
response = operation()
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
355
lms/djangoapps/open_ended_grading/peer_grading_service.py
Normal file
355
lms/djangoapps/open_ended_grading/peer_grading_service.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
This module provides an interface on the grading-service backend
|
||||
for peer grading
|
||||
|
||||
Use peer_grading_service() to get the version specified
|
||||
in settings.PEER_GRADING_INTERFACE
|
||||
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException, ConnectionError, HTTPError
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, Http404
|
||||
from grading_service import GradingService
|
||||
from grading_service import GradingServiceError
|
||||
|
||||
from courseware.access import has_access
|
||||
from util.json_request import expect_json
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
This is a mock peer grading service that can be used for unit tests
|
||||
without making actual service calls to the grading controller
|
||||
"""
|
||||
class MockPeerGradingService(object):
|
||||
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})
|
||||
]})
|
||||
|
||||
class PeerGradingService(GradingService):
|
||||
"""
|
||||
Interface with the grading controller for peer grading
|
||||
"""
|
||||
def __init__(self, config):
|
||||
super(PeerGradingService, self).__init__(config)
|
||||
self.get_next_submission_url = self.url + '/get_next_submission/'
|
||||
self.save_grade_url = self.url + '/save_grade/'
|
||||
self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
|
||||
self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
|
||||
self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
|
||||
self.get_problem_list_url = self.url + '/get_problem_list/'
|
||||
|
||||
def get_next_submission(self, problem_location, grader_id):
|
||||
response = self.get(self.get_next_submission_url, False,
|
||||
{'location': problem_location, 'grader_id': grader_id})
|
||||
return response
|
||||
|
||||
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key):
|
||||
data = {'grader_id' : grader_id,
|
||||
'submission_id' : submission_id,
|
||||
'score' : score,
|
||||
'feedback' : feedback,
|
||||
'submission_key': submission_key,
|
||||
'location': location}
|
||||
return self.post(self.save_grade_url, data)
|
||||
|
||||
def is_student_calibrated(self, problem_location, grader_id):
|
||||
params = {'problem_id' : problem_location, 'student_id': grader_id}
|
||||
return self.get(self.is_student_calibrated_url, params)
|
||||
|
||||
def show_calibration_essay(self, problem_location, grader_id):
|
||||
params = {'problem_id' : problem_location, 'student_id': grader_id}
|
||||
return self.get(self.show_calibration_essay_url, params)
|
||||
|
||||
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback):
|
||||
data = {'location': problem_location,
|
||||
'student_id': grader_id,
|
||||
'calibration_essay_id': calibration_essay_id,
|
||||
'submission_key': submission_key,
|
||||
'score': score,
|
||||
'feedback': feedback}
|
||||
return self.post(self.save_calibration_essay_url, data)
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
params = {'course_id': course_id, 'student_id': grader_id}
|
||||
response = self.get(self.get_problem_list_url, params)
|
||||
return response
|
||||
|
||||
|
||||
_service = None
|
||||
def peer_grading_service():
|
||||
"""
|
||||
Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True,
|
||||
returns a mock one, otherwise a real one.
|
||||
|
||||
Caches the result, so changing the setting after the first call to this
|
||||
function will have no effect.
|
||||
"""
|
||||
global _service
|
||||
if _service is not None:
|
||||
return _service
|
||||
|
||||
if settings.MOCK_PEER_GRADING:
|
||||
_service = MockPeerGradingService()
|
||||
else:
|
||||
_service = PeerGradingService(settings.PEER_GRADING_INTERFACE)
|
||||
|
||||
return _service
|
||||
|
||||
def _err_response(msg):
|
||||
"""
|
||||
Return a HttpResponse with a json dump with success=False, and the given error message.
|
||||
"""
|
||||
return HttpResponse(json.dumps({'success': False, 'error': msg}),
|
||||
mimetype="application/json")
|
||||
|
||||
def _check_required(request, required):
|
||||
actual = set(request.POST.keys())
|
||||
missing = required - actual
|
||||
if len(missing) > 0:
|
||||
return False, "Missing required keys: {0}".format(', '.join(missing))
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
def _check_post(request):
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
|
||||
|
||||
def get_next_submission(request, course_id):
|
||||
"""
|
||||
Makes a call to the grading controller for the next essay that should be graded
|
||||
Returns a json dict with the following keys:
|
||||
|
||||
'success': bool
|
||||
|
||||
'submission_id': a unique identifier for the submission, to be passed back
|
||||
with the grade.
|
||||
|
||||
'submission': the submission, rendered as read-only html for grading
|
||||
|
||||
'rubric': the rubric, also rendered as html.
|
||||
|
||||
'submission_key': a key associated with the submission for validation reasons
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
"""
|
||||
_check_post(request)
|
||||
required = set(['location'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
|
||||
try:
|
||||
response = peer_grading_service().get_next_submission(location, grader_id)
|
||||
return HttpResponse(response,
|
||||
mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
|
||||
.format(staff_grading_service().url, location, grader_id))
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
def save_grade(request, course_id):
|
||||
"""
|
||||
Saves the grade of a given submission.
|
||||
Input:
|
||||
The request should have the following keys:
|
||||
location - problem location
|
||||
submission_id - id associated with this submission
|
||||
submission_key - submission key given for validation purposes
|
||||
score - the grade that was given to the submission
|
||||
feedback - the feedback from the student
|
||||
Returns
|
||||
A json object with the following keys:
|
||||
success: bool indicating whether the save was a success
|
||||
error: if there was an error in the submission, this is the error message
|
||||
"""
|
||||
_check_post(request)
|
||||
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
submission_id = p['submission_id']
|
||||
score = p['score']
|
||||
feedback = p['feedback']
|
||||
submission_key = p['submission_key']
|
||||
try:
|
||||
response = peer_grading_service().save_grade(location, grader_id, submission_id,
|
||||
score, feedback, submission_key)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
|
||||
submission_key: {3}, score: {4}"""
|
||||
.format(staff_grading_service().url,
|
||||
location, submission_id, submission_key, score)
|
||||
)
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
|
||||
|
||||
def is_student_calibrated(request, course_id):
|
||||
"""
|
||||
Calls the grading controller to see if the given student is calibrated
|
||||
on the given problem
|
||||
|
||||
Input:
|
||||
In the request, we need the following arguments:
|
||||
location - problem location
|
||||
|
||||
Returns:
|
||||
Json object with the following keys
|
||||
success - bool indicating whether or not the call was successful
|
||||
calibrated - true if the grader has fully calibrated and can now move on to grading
|
||||
- false if the grader is still working on calibration problems
|
||||
total_calibrated_on_so_far - the number of calibration essays for this problem
|
||||
that this grader has graded
|
||||
"""
|
||||
_check_post(request)
|
||||
required = set(['location'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
|
||||
try:
|
||||
response = peer_grading_service().is_student_calibrated(location, grader_id)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}"
|
||||
.format(staff_grading_service().url, grader_id, location))
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
|
||||
|
||||
def show_calibration_essay(request, course_id):
|
||||
"""
|
||||
Fetch the next calibration essay from the grading controller and return it
|
||||
Inputs:
|
||||
In the request
|
||||
location - problem location
|
||||
|
||||
Returns:
|
||||
A json dict with the following keys
|
||||
'success': bool
|
||||
|
||||
'submission_id': a unique identifier for the submission, to be passed back
|
||||
with the grade.
|
||||
|
||||
'submission': the submission, rendered as read-only html for grading
|
||||
|
||||
'rubric': the rubric, also rendered as html.
|
||||
|
||||
'submission_key': a key associated with the submission for validation reasons
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
|
||||
"""
|
||||
_check_post(request)
|
||||
|
||||
required = set(['location'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
try:
|
||||
response = peer_grading_service().show_calibration_essay(location, grader_id)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}, location: {0}"
|
||||
.format(staff_grading_service().url, location))
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
|
||||
def save_calibration_essay(request, course_id):
|
||||
"""
|
||||
Saves the grader's grade of a given calibration.
|
||||
Input:
|
||||
The request should have the following keys:
|
||||
location - problem location
|
||||
submission_id - id associated with this submission
|
||||
submission_key - submission key given for validation purposes
|
||||
score - the grade that was given to the submission
|
||||
feedback - the feedback from the student
|
||||
Returns
|
||||
A json object with the following keys:
|
||||
success: bool indicating whether the save was a success
|
||||
error: if there was an error in the submission, this is the error message
|
||||
actual_score: the score that the instructor gave to this calibration essay
|
||||
|
||||
"""
|
||||
_check_post(request)
|
||||
|
||||
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
calibration_essay_id = p['submission_id']
|
||||
submission_key = p['submission_key']
|
||||
score = p['score']
|
||||
feedback = p['feedback']
|
||||
|
||||
try:
|
||||
response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, submission_key, score, feedback)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
|
||||
return _err_response('Could not connect to grading service')
|
||||
@@ -7,6 +7,8 @@ import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException, ConnectionError, HTTPError
|
||||
import sys
|
||||
from grading_service import GradingService
|
||||
from grading_service import GradingServiceError
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, Http404
|
||||
@@ -14,13 +16,11 @@ 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 student.models import unique_id_for_user
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MockStaffGradingService(object):
|
||||
"""
|
||||
@@ -57,62 +57,16 @@ class MockStaffGradingService(object):
|
||||
return self.get_next(course_id, 'fake location', grader_id)
|
||||
|
||||
|
||||
class StaffGradingService(object):
|
||||
class StaffGradingService(GradingService):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.username = config['username']
|
||||
self.password = config['password']
|
||||
self.url = config['url']
|
||||
|
||||
self.login_url = self.url + '/login/'
|
||||
super(StaffGradingService, self).__init__(config)
|
||||
self.get_next_url = self.url + '/get_next_submission/'
|
||||
self.save_grade_url = self.url + '/save_grade/'
|
||||
self.get_problem_list_url = self.url + '/get_problem_list/'
|
||||
|
||||
self.session = requests.session()
|
||||
|
||||
|
||||
def _login(self):
|
||||
"""
|
||||
Log into the staff grading service.
|
||||
|
||||
Raises requests.exceptions.HTTPError if something goes wrong.
|
||||
|
||||
Returns the decoded json dict of the response.
|
||||
"""
|
||||
response = self.session.post(self.login_url,
|
||||
{'username': self.username,
|
||||
'password': self.password,})
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json
|
||||
|
||||
|
||||
def _try_with_login(self, operation):
|
||||
"""
|
||||
Call operation(), which should return a requests response object. If
|
||||
the request fails with a 'login_required' error, call _login() and try
|
||||
the operation again.
|
||||
|
||||
Returns the result of operation(). Does not catch exceptions.
|
||||
"""
|
||||
response = operation()
|
||||
if (response.json
|
||||
and response.json.get('success') == False
|
||||
and response.json.get('error') == 'login_required'):
|
||||
# apparrently we aren't logged in. Try to fix that.
|
||||
r = self._login()
|
||||
if r and not r.get('success'):
|
||||
log.warning("Couldn't log into staff_grading backend. Response: %s",
|
||||
r)
|
||||
# try again
|
||||
response = operation()
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
"""
|
||||
@@ -130,17 +84,8 @@ class StaffGradingService(object):
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
op = lambda: self.session.get(self.get_problem_list_url,
|
||||
allow_redirects = False,
|
||||
params={'course_id': course_id,
|
||||
'grader_id': grader_id})
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
params = {'course_id': course_id,'grader_id': grader_id}
|
||||
return self.get(self.get_problem_list_url, params)
|
||||
|
||||
|
||||
def get_next(self, course_id, location, grader_id):
|
||||
@@ -161,17 +106,9 @@ class StaffGradingService(object):
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
op = lambda: self.session.get(self.get_next_url,
|
||||
allow_redirects=False,
|
||||
return self.get(self.get_next_url,
|
||||
params={'location': location,
|
||||
'grader_id': grader_id})
|
||||
try:
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
|
||||
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped):
|
||||
@@ -186,28 +123,20 @@ class StaffGradingService(object):
|
||||
Raises:
|
||||
GradingServiceError if there's a problem connecting.
|
||||
"""
|
||||
try:
|
||||
data = {'course_id': course_id,
|
||||
'submission_id': submission_id,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'grader_id': grader_id,
|
||||
'skipped': skipped}
|
||||
data = {'course_id': course_id,
|
||||
'submission_id': submission_id,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'grader_id': grader_id,
|
||||
'skipped': skipped}
|
||||
|
||||
op = lambda: self.session.post(self.save_grade_url, data=data,
|
||||
allow_redirects=False)
|
||||
r = self._try_with_login(op)
|
||||
except (RequestException, ConnectionError, HTTPError) as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
return self.post(self.save_grade_url, data=data)
|
||||
|
||||
return r.text
|
||||
|
||||
# don't initialize until grading_service() is called--means that just
|
||||
# don't initialize until staff_grading_service() is called--means that just
|
||||
# importing this file doesn't create objects that may not have the right config
|
||||
_service = None
|
||||
|
||||
def grading_service():
|
||||
def staff_grading_service():
|
||||
"""
|
||||
Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
|
||||
returns a mock one, otherwise a real one.
|
||||
@@ -248,7 +177,7 @@ def _check_access(user, course_id):
|
||||
def get_next(request, course_id):
|
||||
"""
|
||||
Get the next thing to grade for course_id and with the location specified
|
||||
in the .
|
||||
in the request.
|
||||
|
||||
Returns a json dict with the following keys:
|
||||
|
||||
@@ -276,11 +205,11 @@ def get_next(request, course_id):
|
||||
if len(missing) > 0:
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
grader_id = request.user.id
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = p['location']
|
||||
|
||||
return HttpResponse(_get_next(course_id, request.user.id, location),
|
||||
return HttpResponse(_get_next(course_id, grader_id, location),
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
@@ -308,12 +237,12 @@ def get_problem_list(request, course_id):
|
||||
"""
|
||||
_check_access(request.user, course_id)
|
||||
try:
|
||||
response = grading_service().get_problem_list(course_id, request.user.id)
|
||||
response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user))
|
||||
return HttpResponse(response,
|
||||
mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}"
|
||||
.format(grading_service().url))
|
||||
.format(staff_grading_service().url))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'}))
|
||||
|
||||
@@ -323,10 +252,10 @@ def _get_next(course_id, grader_id, location):
|
||||
Implementation of get_next (also called from save_grade) -- returns a json string
|
||||
"""
|
||||
try:
|
||||
return grading_service().get_next(course_id, location, grader_id)
|
||||
return staff_grading_service().get_next(course_id, location, grader_id)
|
||||
except GradingServiceError:
|
||||
log.exception("Error from grading service. server url: {0}"
|
||||
.format(grading_service().url))
|
||||
.format(staff_grading_service().url))
|
||||
return json.dumps({'success': False,
|
||||
'error': 'Could not connect to grading service'})
|
||||
|
||||
@@ -357,14 +286,14 @@ def save_grade(request, course_id):
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
|
||||
grader_id = request.user.id
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
|
||||
|
||||
location = p['location']
|
||||
skipped = 'skipped' in p
|
||||
try:
|
||||
result_json = grading_service().save_grade(course_id,
|
||||
result_json = staff_grading_service().save_grade(course_id,
|
||||
grader_id,
|
||||
p['submission_id'],
|
||||
p['score'],
|
||||
112
lms/djangoapps/open_ended_grading/tests.py
Normal file
112
lms/djangoapps/open_ended_grading/tests.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Tests for open ended grading interfaces
|
||||
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open_ended_grading
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from open_ended_grading import staff_grading_service
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
from nose import SkipTest
|
||||
from mock import patch, Mock
|
||||
import json
|
||||
|
||||
from override_settings import override_settings
|
||||
|
||||
_mock_service = staff_grading_service.MockStaffGradingService()
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestStaffGradingService(ct.PageLoader):
|
||||
'''
|
||||
Check that staff grading service proxy works. Basically just checking the
|
||||
access control and error handling logic -- all the actual work is on the
|
||||
backend.
|
||||
'''
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
self.student = 'view@test.com'
|
||||
self.instructor = 'view2@test.com'
|
||||
self.password = 'foo'
|
||||
self.location = 'TestLocation'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.create_account('u2', self.instructor, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
def make_instructor(course):
|
||||
group_name = _course_staff_group_name(course.location)
|
||||
g = Group.objects.create(name=group_name)
|
||||
g.user_set.add(ct.user(self.instructor))
|
||||
|
||||
make_instructor(self.toy)
|
||||
|
||||
self.mock_service = staff_grading_service.staff_grading_service()
|
||||
|
||||
self.logout()
|
||||
|
||||
def test_access(self):
|
||||
"""
|
||||
Make sure only staff have access.
|
||||
"""
|
||||
self.login(self.student, self.password)
|
||||
|
||||
# both get and post should return 404
|
||||
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
||||
url = reverse(view_name, kwargs={'course_id': self.course_id})
|
||||
self.check_for_get_code(404, url)
|
||||
self.check_for_post_code(404, url)
|
||||
|
||||
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
||||
data = {'location': self.location}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'])
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
self.assertIsNotNone(d['submission'])
|
||||
self.assertIsNotNone(d['num_graded'])
|
||||
self.assertIsNotNone(d['min_for_ml'])
|
||||
self.assertIsNotNone(d['num_pending'])
|
||||
self.assertIsNotNone(d['prompt'])
|
||||
self.assertIsNotNone(d['ml_error_info'])
|
||||
self.assertIsNotNone(d['max_score'])
|
||||
self.assertIsNotNone(d['rubric'])
|
||||
|
||||
|
||||
def test_save_grade(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
|
||||
data = {'score': '12',
|
||||
'feedback': 'great!',
|
||||
'submission_id': '123',
|
||||
'location': self.location}
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertEquals(d['submission_id'], self.mock_service.cnt)
|
||||
|
||||
def test_get_problem_list(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
|
||||
data = {}
|
||||
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertIsNotNone(d['problem_list'])
|
||||
118
lms/djangoapps/open_ended_grading/views.py
Normal file
118
lms/djangoapps/open_ended_grading/views.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# Grading Views
|
||||
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from django.conf import settings
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
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 grading_service import GradingServiceError
|
||||
import json
|
||||
from .staff_grading import StaffGrading
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
if settings.MOCK_PEER_GRADING:
|
||||
peer_gs = MockPeerGradingService()
|
||||
else:
|
||||
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
|
||||
|
||||
"""
|
||||
Reverses the URL from the name and the course id, and then adds a trailing slash if
|
||||
it does not exist yet
|
||||
|
||||
"""
|
||||
def _reverse_with_slash(url_name, course_id):
|
||||
ajax_url = reverse(url_name, kwargs={'course_id': course_id})
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
return ajax_url
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def staff_grading(request, course_id):
|
||||
"""
|
||||
Show the instructor grading interface.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
ajax_url = _reverse_with_slash('staff_grading', course_id)
|
||||
|
||||
return render_to_response('instructor/staff_grading.html', {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
# 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')
|
||||
|
||||
# call problem list service
|
||||
success = False
|
||||
error_text = ""
|
||||
problem_list = []
|
||||
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)
|
||||
|
||||
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, })
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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, })
|
||||
|
||||
|
||||
@@ -332,6 +332,9 @@ STAFF_GRADING_INTERFACE = None
|
||||
# Used for testing, debugging
|
||||
MOCK_STAFF_GRADING = False
|
||||
|
||||
################################# Peer grading config #####################
|
||||
PEER_GRADING_INTERFACE = None
|
||||
MOCK_PEER_GRADING = False
|
||||
|
||||
################################# Jasmine ###################################
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
@@ -410,9 +413,8 @@ 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'))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
@@ -438,11 +440,12 @@ PIPELINE_CSS = {
|
||||
PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/course.scss']
|
||||
PIPELINE_JS = {
|
||||
'application': {
|
||||
|
||||
# Application will contain all paths not in courseware_only_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)
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
|
||||
) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
@@ -471,8 +474,11 @@ PIPELINE_JS = {
|
||||
'staff_grading' : {
|
||||
'source_filenames': staff_grading_js,
|
||||
'output_filename': 'js/staff_grading.js'
|
||||
},
|
||||
'peer_grading' : {
|
||||
'source_filenames': peer_grading_js,
|
||||
'output_filename': 'js/peer_grading.js'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PIPELINE_DISABLE_WRAPPER = True
|
||||
@@ -545,6 +551,7 @@ INSTALLED_APPS = (
|
||||
'util',
|
||||
'certificates',
|
||||
'instructor',
|
||||
'open_ended_grading',
|
||||
'psychometrics',
|
||||
'licenses',
|
||||
|
||||
|
||||
@@ -114,6 +114,13 @@ STAFF_GRADING_INTERFACE = {
|
||||
'password': 'abcd',
|
||||
}
|
||||
|
||||
################################# Peer grading config #####################
|
||||
|
||||
PEER_GRADING_INTERFACE = {
|
||||
'url': 'http://127.0.0.1:3033/peer_grading',
|
||||
'username': 'lms',
|
||||
'password': 'abcd',
|
||||
}
|
||||
################################ LMS Migration #################################
|
||||
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
|
||||
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
|
||||
|
||||
@@ -62,6 +62,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
|
||||
|
||||
# Don't rely on a real staff grading backend
|
||||
MOCK_STAFF_GRADING = True
|
||||
MOCK_PEER_GRADING = True
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
||||
# into common.py so that we don't have to repeat this sort of thing
|
||||
|
||||
@@ -422,7 +422,8 @@ class formula(object):
|
||||
|
||||
def GetContentMathML(self, asciimath, mathml):
|
||||
# URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
|
||||
URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
|
||||
# URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
|
||||
URL = 'https://math-xserver.mitx.mit.edu/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
|
||||
|
||||
if 1:
|
||||
payload = {'asciiMathInput': asciimath,
|
||||
@@ -430,7 +431,7 @@ class formula(object):
|
||||
#'asciiMathML':unicode(mathml).encode('utf-8'),
|
||||
}
|
||||
headers = {'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"}
|
||||
r = requests.post(URL, data=payload, headers=headers)
|
||||
r = requests.post(URL, data=payload, headers=headers, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
ret = r.text
|
||||
#print "encoding: ",r.encoding
|
||||
|
||||
13
lms/static/coffee/src/peer_grading/peer_grading.coffee
Normal file
13
lms/static/coffee/src/peer_grading/peer_grading.coffee
Normal file
@@ -0,0 +1,13 @@
|
||||
# This is a simple class that just hides the error container
|
||||
# and message container when they are empty
|
||||
# Can (and should be) expanded upon when our problem list
|
||||
# becomes more sophisticated
|
||||
class PeerGrading
|
||||
constructor: () ->
|
||||
@error_container = $('.error-container')
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
$(document).ready(() -> new PeerGrading())
|
||||
390
lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
Normal file
390
lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
Normal file
@@ -0,0 +1,390 @@
|
||||
##################################
|
||||
#
|
||||
# This is the JS that renders the peer grading problem page.
|
||||
# Fetches the correct problem and/or calibration essay
|
||||
# and sends back the grades
|
||||
#
|
||||
# Should not be run when we don't have a location to send back
|
||||
# to the server
|
||||
#
|
||||
# PeerGradingProblemBackend -
|
||||
# makes all the ajax requests and provides a mock interface
|
||||
# for testing purposes
|
||||
#
|
||||
# PeerGradingProblem -
|
||||
# handles the rendering and user interactions with the interface
|
||||
#
|
||||
##################################
|
||||
class PeerGradingProblemBackend
|
||||
constructor: (ajax_url, mock_backend) ->
|
||||
@mock_backend = mock_backend
|
||||
@ajax_url = ajax_url
|
||||
@mock_cnt = 0
|
||||
|
||||
post: (cmd, data, callback) ->
|
||||
if @mock_backend
|
||||
callback(@mock(cmd, data))
|
||||
else
|
||||
# if this post request fails, the error callback will catch it
|
||||
$.post(@ajax_url + cmd, data, callback)
|
||||
.error => callback({success: false, error: "Error occured while performing this operation"})
|
||||
|
||||
mock: (cmd, data) ->
|
||||
if cmd == 'is_student_calibrated'
|
||||
# change to test each version
|
||||
response =
|
||||
success: true
|
||||
calibrated: @mock_cnt >= 2
|
||||
else if cmd == 'show_calibration_essay'
|
||||
#response =
|
||||
# success: false
|
||||
# error: "There was an error"
|
||||
@mock_cnt++
|
||||
response =
|
||||
success: true
|
||||
submission_id: 1
|
||||
submission_key: 'abcd'
|
||||
student_response: '''
|
||||
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
|
||||
|
||||
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
|
||||
'''
|
||||
prompt: '''
|
||||
<h2>S11E3: Metal Bands</h2>
|
||||
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
|
||||
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
|
||||
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
|
||||
'''
|
||||
rubric: '''
|
||||
<ul>
|
||||
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
|
||||
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
|
||||
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
|
||||
</ul>
|
||||
|
||||
<p>Please score your response according to how many of the above components you identified:</p>
|
||||
'''
|
||||
max_score: 4
|
||||
else if cmd == 'get_next_submission'
|
||||
response =
|
||||
success: true
|
||||
submission_id: 1
|
||||
submission_key: 'abcd'
|
||||
student_response: '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec tristique ante. Proin at mauris sapien, quis varius leo. Morbi laoreet leo nisi. Morbi aliquam lacus ante. Cras iaculis velit sed diam mattis a fermentum urna luctus. Duis consectetur nunc vitae felis facilisis eget vulputate risus viverra. Cras consectetur ullamcorper lobortis. Nam eu gravida lorem. Nulla facilisi. Nullam quis felis enim. Mauris orci lectus, dictum id cursus in, vulputate in massa.
|
||||
|
||||
Phasellus non varius sem. Nullam commodo lacinia odio sit amet egestas. Donec ullamcorper sapien sagittis arcu volutpat placerat. Phasellus ut pretium ante. Nam dictum pulvinar nibh dapibus tristique. Sed at tellus mi, fringilla convallis justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique rutrum nulla sed eleifend. Praesent at nunc arcu. Mauris condimentum faucibus nibh, eget commodo quam viverra sed. Morbi in tincidunt dolor. Morbi sed augue et augue interdum fermentum.
|
||||
|
||||
Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim quis placerat at, aliquet ac metus. Mauris vulputate est eu nibh imperdiet varius. Cras aliquet rhoncus elit a laoreet. Mauris consectetur erat et erat scelerisque eu faucibus dolor consequat. Nam adipiscing sagittis nisl, eu mollis massa tempor ac. Nulla scelerisque tempus blandit. Phasellus ac ipsum eros, id posuere arcu. Nullam non sapien arcu. Vivamus sit amet lorem justo, ac tempus turpis. Suspendisse pharetra gravida imperdiet. Pellentesque lacinia mi eu elit luctus pellentesque. Sed accumsan libero a magna elementum varius. Nunc eget pellentesque metus. '''
|
||||
prompt: '''
|
||||
<h2>S11E3: Metal Bands</h2>
|
||||
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
|
||||
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
|
||||
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
|
||||
'''
|
||||
rubric: '''
|
||||
<ul>
|
||||
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
|
||||
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
|
||||
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
|
||||
</ul>
|
||||
|
||||
<p>Please score your response according to how many of the above components you identified:</p>
|
||||
'''
|
||||
max_score: 4
|
||||
else if cmd == 'save_calibration_essay'
|
||||
response =
|
||||
success: true
|
||||
actual_score: 2
|
||||
else if cmd == 'save_grade'
|
||||
response =
|
||||
success: true
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class PeerGradingProblem
|
||||
constructor: (backend) ->
|
||||
@prompt_wrapper = $('.prompt-wrapper')
|
||||
@backend = backend
|
||||
|
||||
|
||||
# get the location of the problem
|
||||
@location = $('.peer-grading').data('location')
|
||||
# prevent this code from trying to run
|
||||
# when we don't have a location
|
||||
if(!@location)
|
||||
return
|
||||
|
||||
# get the other elements we want to fill in
|
||||
@submission_container = $('.submission-container')
|
||||
@prompt_container = $('.prompt-container')
|
||||
@rubric_container = $('.rubric-container')
|
||||
@calibration_panel = $('.calibration-panel')
|
||||
@grading_panel = $('.grading-panel')
|
||||
@content_panel = $('.content-panel')
|
||||
@grading_message = $('.grading-message')
|
||||
@grading_message.hide()
|
||||
|
||||
@grading_wrapper =$('.grading-wrapper')
|
||||
@calibration_feedback_panel = $('.calibration-feedback')
|
||||
@interstitial_page = $('.interstitial-page')
|
||||
@interstitial_page.hide()
|
||||
|
||||
@error_container = $('.error-container')
|
||||
|
||||
@submission_key_input = $("input[name='submission-key']")
|
||||
@essay_id_input = $("input[name='essay-id']")
|
||||
@feedback_area = $('.feedback-area')
|
||||
|
||||
@score_selection_container = $('.score-selection-container')
|
||||
@score = null
|
||||
@calibration = null
|
||||
|
||||
@submit_button = $('.submit-button')
|
||||
@action_button = $('.action-button')
|
||||
@calibration_feedback_button = $('.calibration-feedback-button')
|
||||
@interstitial_page_button = $('.interstitial-page-button')
|
||||
|
||||
Collapsible.setCollapsibles(@content_panel)
|
||||
|
||||
# Set up the click event handlers
|
||||
@action_button.click -> history.back()
|
||||
@calibration_feedback_button.click =>
|
||||
@calibration_feedback_panel.hide()
|
||||
@grading_wrapper.show()
|
||||
@is_calibrated_check()
|
||||
|
||||
@interstitial_page_button.click =>
|
||||
@interstitial_page.hide()
|
||||
@is_calibrated_check()
|
||||
|
||||
@is_calibrated_check()
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Ajax calls to the backend
|
||||
#
|
||||
##########
|
||||
is_calibrated_check: () =>
|
||||
@backend.post('is_student_calibrated', {location: @location}, @calibration_check_callback)
|
||||
|
||||
fetch_calibration_essay: () =>
|
||||
@backend.post('show_calibration_essay', {location: @location}, @render_calibration)
|
||||
|
||||
fetch_submission_essay: () =>
|
||||
@backend.post('get_next_submission', {location: @location}, @render_submission)
|
||||
|
||||
construct_data: () ->
|
||||
data =
|
||||
score: @score
|
||||
location: @location
|
||||
submission_id: @essay_id_input.val()
|
||||
submission_key: @submission_key_input.val()
|
||||
feedback: @feedback_area.val()
|
||||
return data
|
||||
|
||||
|
||||
submit_calibration_essay: ()=>
|
||||
data = @construct_data()
|
||||
@backend.post('save_calibration_essay', data, @calibration_callback)
|
||||
|
||||
submit_grade: () =>
|
||||
data = @construct_data()
|
||||
@backend.post('save_grade', data, @submission_callback)
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Callbacks for various events
|
||||
#
|
||||
##########
|
||||
|
||||
# called after we perform an is_student_calibrated check
|
||||
calibration_check_callback: (response) =>
|
||||
if response.success
|
||||
# if we haven't been calibrating before
|
||||
if response.calibrated and (@calibration == null or @calibration == false)
|
||||
@calibration = false
|
||||
@fetch_submission_essay()
|
||||
# If we were calibrating before and no longer need to,
|
||||
# show the interstitial page
|
||||
else if response.calibrated and @calibration == true
|
||||
@calibration = false
|
||||
@render_interstitial_page()
|
||||
else
|
||||
@calibration = true
|
||||
@fetch_calibration_essay()
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error contacting the grading service")
|
||||
|
||||
|
||||
# called after we submit a calibration score
|
||||
calibration_callback: (response) =>
|
||||
if response.success
|
||||
@render_calibration_feedback(response)
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error saving calibration score")
|
||||
|
||||
# called after we submit a submission score
|
||||
submission_callback: (response) =>
|
||||
if response.success
|
||||
@is_calibrated_check()
|
||||
@grading_message.fadeIn()
|
||||
@grading_message.html("<p>Grade sent successfully.</p>")
|
||||
else
|
||||
if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("Error occurred while submitting grade")
|
||||
|
||||
# called after a grade is selected on the interface
|
||||
graded_callback: (event) =>
|
||||
@grading_message.hide()
|
||||
@score = event.target.value
|
||||
@show_submit_button()
|
||||
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
# Rendering methods and helpers
|
||||
#
|
||||
##########
|
||||
# renders a calibration essay
|
||||
render_calibration: (response) =>
|
||||
if response.success
|
||||
|
||||
# load in all the data
|
||||
@submission_container.html("<h3>Training Essay</h3>")
|
||||
@render_submission_data(response)
|
||||
# TODO: indicate that we're in calibration mode
|
||||
@calibration_panel.addClass('current-state')
|
||||
@grading_panel.removeClass('current-state')
|
||||
|
||||
# Display the right text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').show()
|
||||
@grading_panel.find('.calibration-text').show()
|
||||
@calibration_panel.find('.grading-text').hide()
|
||||
@grading_panel.find('.grading-text').hide()
|
||||
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_calibration_essay
|
||||
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("An error occurred while retrieving the next calibration essay")
|
||||
|
||||
# Renders a student submission to be graded
|
||||
render_submission: (response) =>
|
||||
if response.success
|
||||
@submit_button.hide()
|
||||
@submission_container.html("<h3>Submitted Essay</h3>")
|
||||
@render_submission_data(response)
|
||||
|
||||
@calibration_panel.removeClass('current-state')
|
||||
@grading_panel.addClass('current-state')
|
||||
|
||||
# Display the correct text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').hide()
|
||||
@grading_panel.find('.calibration-text').hide()
|
||||
@calibration_panel.find('.grading-text').show()
|
||||
@grading_panel.find('.grading-text').show()
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_grade
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@render_error("An error occured when retrieving the next submission.")
|
||||
|
||||
|
||||
make_paragraphs: (text) ->
|
||||
paragraph_split = text.split(/\n\s*\n/)
|
||||
new_text = ''
|
||||
for paragraph in paragraph_split
|
||||
new_text += "<p>#{paragraph}</p>"
|
||||
return new_text
|
||||
|
||||
# render common information between calibration and grading
|
||||
render_submission_data: (response) =>
|
||||
@content_panel.show()
|
||||
|
||||
@submission_container.append(@make_paragraphs(response.student_response))
|
||||
@prompt_container.html(response.prompt)
|
||||
@rubric_container.html(response.rubric)
|
||||
@submission_key_input.val(response.submission_key)
|
||||
@essay_id_input.val(response.submission_id)
|
||||
@setup_score_selection(response.max_score)
|
||||
|
||||
@submit_button.hide()
|
||||
@action_button.hide()
|
||||
@calibration_feedback_panel.hide()
|
||||
|
||||
|
||||
render_calibration_feedback: (response) =>
|
||||
# display correct grade
|
||||
@calibration_feedback_panel.slideDown()
|
||||
calibration_wrapper = $('.calibration-feedback-wrapper')
|
||||
calibration_wrapper.html("<p>The score you gave was: #{@score}. The actual score is: #{response.actual_score}</p>")
|
||||
|
||||
|
||||
score = parseInt(@score)
|
||||
actual_score = parseInt(response.actual_score)
|
||||
|
||||
if score == actual_score
|
||||
calibration_wrapper.append("<p>Congratulations! Your score matches the actual score!</p>")
|
||||
else
|
||||
calibration_wrapper.append("<p>Please try to understand the grading critera better to be more accurate next time.</p>")
|
||||
|
||||
# disable score selection and submission from the grading interface
|
||||
$("input[name='score-selection']").attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
|
||||
render_interstitial_page: () =>
|
||||
@content_panel.hide()
|
||||
@interstitial_page.show()
|
||||
|
||||
render_error: (error_message) =>
|
||||
@error_container.show()
|
||||
@calibration_feedback_panel.hide()
|
||||
@error_container.html(error_message)
|
||||
@content_panel.hide()
|
||||
@action_button.show()
|
||||
|
||||
show_submit_button: () =>
|
||||
@submit_button.show()
|
||||
|
||||
setup_score_selection: (max_score) =>
|
||||
# first, get rid of all the old inputs, if any.
|
||||
@score_selection_container.html('Choose score: ')
|
||||
|
||||
# Now create new labels and inputs for each possible score.
|
||||
for score in [0..max_score]
|
||||
id = 'score-' + score
|
||||
label = """<label for="#{id}">#{score}</label>"""
|
||||
|
||||
input = """
|
||||
<input type="radio" name="score-selection" id="#{id}" value="#{score}"/>
|
||||
""" # " fix broken parsing in emacs
|
||||
@score_selection_container.append(input + label)
|
||||
|
||||
# And now hook up an event handler again
|
||||
$("input[name='score-selection']").change @graded_callback
|
||||
|
||||
|
||||
|
||||
mock_backend = false
|
||||
ajax_url = $('.peer-grading').data('ajax_url')
|
||||
backend = new PeerGradingProblemBackend(ajax_url, mock_backend)
|
||||
$(document).ready(() -> new PeerGradingProblem(backend))
|
||||
@@ -1,4 +1,5 @@
|
||||
div.staff-grading {
|
||||
div.staff-grading,
|
||||
div.peer-grading{
|
||||
textarea.feedback-area {
|
||||
height: 75px;
|
||||
margin: 20px;
|
||||
@@ -36,8 +37,8 @@ div.staff-grading {
|
||||
}
|
||||
|
||||
.prompt-information-container,
|
||||
.submission-wrapper,
|
||||
.rubric-wrapper,
|
||||
.calibration-feedback-wrapper,
|
||||
.grading-container
|
||||
{
|
||||
border: 1px solid gray;
|
||||
@@ -49,6 +50,18 @@ div.staff-grading {
|
||||
padding: 15px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.submission-wrapper
|
||||
{
|
||||
h3
|
||||
{
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p
|
||||
{
|
||||
margin-left:10px;
|
||||
}
|
||||
padding: 15px;
|
||||
}
|
||||
.meta-info-wrapper
|
||||
{
|
||||
background-color: #eee;
|
||||
@@ -67,7 +80,8 @@ div.staff-grading {
|
||||
}
|
||||
}
|
||||
}
|
||||
.message-container
|
||||
.message-container,
|
||||
.grading-message
|
||||
{
|
||||
background-color: $yellow;
|
||||
padding: 10px;
|
||||
@@ -81,6 +95,69 @@ div.staff-grading {
|
||||
margin-bottom:5px;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.instructions-panel
|
||||
{
|
||||
|
||||
margin-right:20px;
|
||||
> div
|
||||
{
|
||||
padding: 10px;
|
||||
margin: 0px;
|
||||
background: #eee;
|
||||
height: 10em;
|
||||
h3
|
||||
{
|
||||
text-align:center;
|
||||
text-transform:uppercase;
|
||||
color: #777;
|
||||
}
|
||||
p
|
||||
{
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
.calibration-panel
|
||||
{
|
||||
float:left;
|
||||
width:48%;
|
||||
}
|
||||
.grading-panel
|
||||
{
|
||||
float:right;
|
||||
width: 48%;
|
||||
}
|
||||
.current-state
|
||||
{
|
||||
background: #1D9DD9;
|
||||
h3, p
|
||||
{
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
|
||||
.collapsible
|
||||
{
|
||||
margin-left: 0px;
|
||||
header
|
||||
{
|
||||
margin-top:20px;
|
||||
margin-bottom:20px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.interstitial-page
|
||||
{
|
||||
text-align: center;
|
||||
input[type=button]
|
||||
{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
%>
|
||||
<%page args="course" />
|
||||
<article id="${course.id}" class="course">
|
||||
%if course.metadata.get('is_new'):
|
||||
%if course.is_new:
|
||||
<span class="status">New</span>
|
||||
%endif
|
||||
<a href="${reverse('about_course', args=[course.id])}">
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
</div>
|
||||
<div class="message-container">
|
||||
</div>
|
||||
|
||||
<! -- Problem List View -->
|
||||
<section class="problem-list-container">
|
||||
<h2>Instructions</h2>
|
||||
<div class="instructions">
|
||||
@@ -35,6 +37,8 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Grading View -->
|
||||
|
||||
<section class="prompt-wrapper">
|
||||
<h2 class="prompt-name"></h2>
|
||||
<div class="meta-info-wrapper">
|
||||
|
||||
39
lms/templates/peer_grading/peer_grading.html
Normal file
39
lms/templates/peer_grading/peer_grading.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<%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='staff_grading'" />
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:js group='peer_grading'/>
|
||||
</%block>
|
||||
|
||||
<section class="container">
|
||||
<div class="peer-grading" data-ajax_url="${ajax_url}">
|
||||
<div class="error-container">${error_text}</div>
|
||||
<h1>Peer Grading</h1>
|
||||
<h2>Instructions</h2>
|
||||
<p>Here are a list of problems that need to be peer graded for this course.</p>
|
||||
% if success:
|
||||
% if len(problem_list) == 0:
|
||||
<div class="message-container">
|
||||
Nothing to grade!
|
||||
</div>
|
||||
%else:
|
||||
<ul class="problem-list">
|
||||
%for problem in problem_list:
|
||||
<li>
|
||||
<a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']} (${problem['num_graded']} graded, ${problem['num_pending']} pending)</a>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
%endif
|
||||
%endif
|
||||
</div>
|
||||
</section>
|
||||
112
lms/templates/peer_grading/peer_grading_problem.html
Normal file
112
lms/templates/peer_grading/peer_grading_problem.html
Normal file
@@ -0,0 +1,112 @@
|
||||
|
||||
<%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='staff_grading'" />
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:js group='peer_grading'/>
|
||||
</%block>
|
||||
|
||||
|
||||
<section class="container">
|
||||
<div class="peer-grading" data-ajax_url="${ajax_url}" data-location="${problem_location}">
|
||||
<div class="error-container"></div>
|
||||
|
||||
<section class="content-panel">
|
||||
<h1>Peer Grading </h1>
|
||||
<div class="instructions-panel">
|
||||
<div class="calibration-panel">
|
||||
<h3>Learning to Grade</h3>
|
||||
<div class="calibration-text">
|
||||
<p>Before you can do any proper peer grading, you first need to understand how your own grading compares to that of the instrutor. Once your grades begin to match the instructor's, you will move on to grading your peers!</p>
|
||||
</div>
|
||||
<div class="grading-text">
|
||||
<p>You have successfully managed to calibrate your answers to that of the instructors and have moved onto the next step in the peer grading process.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grading-panel">
|
||||
<h3>Grading</h3>
|
||||
<div class="calibration-text">
|
||||
<p>You cannot start grading until you have graded a sufficient number of training problems and have been able to demonstrate that your scores closely match that of the instructor.</p>
|
||||
</div>
|
||||
<div class="grading-text">
|
||||
<p>Now that you have finished your training, you are now allowed to grade your peers. Please keep in mind that students are allowed to respond to the grades and feedback they receive.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prompt-wrapper">
|
||||
<div class="prompt-information-container collapsible">
|
||||
<header><a href="javascript:void(0)">Question</a></header>
|
||||
<section>
|
||||
<div class="prompt-container">
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="rubric-wrapper collapsible">
|
||||
<header><a href="javascript:void(0)">Rubric</a></header>
|
||||
<section>
|
||||
<div class="rubric-container">
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<section class="grading-wrapper">
|
||||
<h2>Grading</h2>
|
||||
|
||||
<div class="grading-container">
|
||||
<div class="submission-wrapper">
|
||||
<h3></h3>
|
||||
<div class="submission-container">
|
||||
</div>
|
||||
<input type="hidden" name="submission-key" value="" />
|
||||
<input type="hidden" name="essay-id" value="" />
|
||||
</div>
|
||||
<div class="evaluation">
|
||||
<p class="score-selection-container">
|
||||
</p>
|
||||
<textarea name="feedback" placeholder="Feedback for student (optional)"
|
||||
class="feedback-area" cols="70" ></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="submission">
|
||||
<input type="button" value="Submit" class="submit-button" name="show"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grading-message">
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Calibration feedback: Shown after a calibration is sent -->
|
||||
<section class="calibration-feedback">
|
||||
<h2>How did I do?</h2>
|
||||
<div class="calibration-feedback-wrapper">
|
||||
</div>
|
||||
<input type="button" class="calibration-feedback-button" value="Continue" name="calibration-feedback-button" />
|
||||
</section>
|
||||
|
||||
<!-- Interstitial Page: Shown between calibration and grading steps -->
|
||||
<section class="interstitial-page">
|
||||
<h1>Congratulations!</h1>
|
||||
<p> You have now completed the calibration step. You are now ready to start grading.</p>
|
||||
<input type="button" class="interstitial-page-button" value="Start Grading!" name="interstitial-page-button" />
|
||||
</section>
|
||||
|
||||
<input type="button" value="Go Back" class="action-button" name="back" />
|
||||
</div>
|
||||
</section>
|
||||
@@ -21,7 +21,7 @@
|
||||
<article class="response">
|
||||
<h3>What is edX?</h3>
|
||||
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
|
||||
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.</p>
|
||||
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning both on-campus and online throughout the world.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="inner-wrapper">
|
||||
<h3>EdX is looking to add new talent to our team! </h3>
|
||||
<p align="center"><em>Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status</em></p>
|
||||
<p>Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access free education. We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.</p>
|
||||
<p>Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access to free education. We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.</p>
|
||||
<p>Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.</p>
|
||||
<p>As part of the edX team, you’ll receive:</p>
|
||||
<ul>
|
||||
|
||||
27
lms/urls.py
27
lms/urls.py
@@ -243,15 +243,32 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
|
||||
'instructor.views.enroll_students', name='enroll_students'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading$',
|
||||
'instructor.views.staff_grading', name='staff_grading'),
|
||||
'open_ended_grading.views.staff_grading', name='staff_grading'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_next$',
|
||||
'instructor.staff_grading_service.get_next', name='staff_grading_get_next'),
|
||||
'open_ended_grading.staff_grading_service.get_next', name='staff_grading_get_next'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
|
||||
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
|
||||
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
'open_ended_grading.staff_grading_service.save_grade', name='staff_grading_save_grade'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$',
|
||||
'instructor.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
|
||||
'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
|
||||
|
||||
|
||||
# Peer Grading
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
|
||||
'open_ended_grading.views.peer_grading', name='peer_grading'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/problem$',
|
||||
'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$',
|
||||
'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$',
|
||||
'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/is_student_calibrated$',
|
||||
'open_ended_grading.peer_grading_service.is_student_calibrated', name='peer_grading_is_student_calibrated'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_grade$',
|
||||
'open_ended_grading.peer_grading_service.save_grade', name='peer_grading_save_grade'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_calibration_essay$',
|
||||
'open_ended_grading.peer_grading_service.save_calibration_essay', name='peer_grading_save_calibration_essay'),
|
||||
)
|
||||
|
||||
# discussion forums live within courseware, so courseware must be enabled first
|
||||
|
||||
Reference in New Issue
Block a user