Commit 4efe5789 by chrisndodge

Merge pull request #1269 from MITx/feature/cdodge/cms-master-merge3

Feature/cdodge/cms master merge3
parents d0c4e2e9 9479ef3c
...@@ -202,7 +202,7 @@ def edit_subsection(request, location): ...@@ -202,7 +202,7 @@ def edit_subsection(request, location):
if item.location.category != 'sequential': if item.location.category != 'sequential':
return HttpResponseBadRequest() 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 # we're for now assuming a single parent
if len(parent_locs) != 1: if len(parent_locs) != 1:
...@@ -285,10 +285,10 @@ def edit_unit(request, location): ...@@ -285,10 +285,10 @@ def edit_unit(request, location):
# this will need to change to check permissions correctly so as # this will need to change to check permissions correctly so as
# to pick the correct parent subsection # 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_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]) containing_section = modulestore().get_item(containing_section_locs[0])
# cdodge hack. We're having trouble previewing drafts via jump_to redirect # cdodge hack. We're having trouble previewing drafts via jump_to redirect
......
...@@ -78,7 +78,7 @@ def index(request, extra_context={}, user=None): ...@@ -78,7 +78,7 @@ def index(request, extra_context={}, user=None):
courses = get_courses(None, domain=domain) courses = get_courses(None, domain=domain)
# Sort courses by how far are they from they start day # 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) courses = sorted(courses, key=key, reverse=True)
# Get the 3 most recent news # Get the 3 most recent news
......
...@@ -633,7 +633,7 @@ class MultipleChoiceResponse(LoncapaResponse): ...@@ -633,7 +633,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# define correct choices (after calling secondary setup) # define correct choices (after calling secondary setup)
xml = self.xml xml = self.xml
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) 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): def mc_setup_response(self):
''' '''
...@@ -727,7 +727,7 @@ class OptionResponse(LoncapaResponse): ...@@ -727,7 +727,7 @@ class OptionResponse(LoncapaResponse):
return cmap return cmap
def get_answers(self): 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)) # log.debug('%s: expected answers=%s' % (unicode(self),amap))
return amap return amap
......
import logging
from cStringIO import StringIO from cStringIO import StringIO
from lxml import etree from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus from path import path # NOTE (THK): Only used for detecting presence of syllabus
from xmodule.graders import grader_from_conf import requests
import time
from datetime import datetime
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time, stringify_time from xmodule.timeparse import parse_time, stringify_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
from datetime import datetime from datetime import datetime
import json import json
import logging import logging
...@@ -16,11 +21,13 @@ import copy ...@@ -16,11 +21,13 @@ import copy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True) remove_comments=True, remove_blank_text=True)
_cached_toc = {} _cached_toc = {}
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
...@@ -334,6 +341,38 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -334,6 +341,38 @@ class CourseDescriptor(SequenceDescriptor):
def show_calculator(self): def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes" 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 @lazyproperty
def grading_context(self): def grading_context(self):
""" """
...@@ -413,7 +452,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -413,7 +452,6 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("{0} is not a course location".format(loc)) raise ValueError("{0} is not a course location".format(loc))
return "/".join([loc.org, loc.course, loc.name]) return "/".join([loc.org, loc.course, loc.name])
@property @property
def id(self): def id(self):
"""Return the course_id for this course""" """Return the course_id for this course"""
...@@ -511,5 +549,3 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -511,5 +549,3 @@ class CourseDescriptor(SequenceDescriptor):
def org(self): def org(self):
return self.location.org return self.location.org
...@@ -364,9 +364,9 @@ class ModuleStore(object): ...@@ -364,9 +364,9 @@ class ModuleStore(object):
''' '''
raise NotImplementedError raise NotImplementedError
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 '''Find all locations that are the parents of this location in this
for path_to_location(). course. Needed for path_to_location().
returns an iterable of things that can be passed to Location. returns an iterable of things that can be passed to Location.
''' '''
......
...@@ -160,13 +160,13 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -160,13 +160,13 @@ class DraftModuleStore(ModuleStoreBase):
return super(DraftModuleStore, self).delete_item(as_draft(location)) 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 '''Find all locations that are the parents of this location. Needed
for path_to_location(). for path_to_location().
returns an iterable of things that can be passed 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): def publish(self, location, published_by_id):
""" """
......
...@@ -402,6 +402,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -402,6 +402,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata}) self._update_single_item(location, {'metadata': metadata})
def delete_item(self, location): def delete_item(self, location):
""" """
Delete an item from this modulestore Delete an item from this modulestore
...@@ -420,12 +421,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -420,12 +421,10 @@ class MongoModuleStore(ModuleStoreBase):
self.collection.remove({'_id': Location(location).dict()}) 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 def get_parent_locations(self, location, course_id):
be empty if there are no parents. '''Find all locations that are the parents of this location in this
course. Needed for path_to_location().
''' '''
location = Location.ensure_fully_specified(location) location = Location.ensure_fully_specified(location)
items = self.collection.find({'definition.children': location.url()}, items = self.collection.find({'definition.children': location.url()},
......
...@@ -60,9 +60,11 @@ def path_to_location(modulestore, course_id, location): ...@@ -60,9 +60,11 @@ def path_to_location(modulestore, course_id, location):
(loc, path) = queue.pop() # Takes from the end (loc, path) = queue.pop() # Takes from the end
loc = Location(loc) loc = Location(loc)
# Call get_parent_locations first to make sure the location is there # get_parent_locations should raise ItemNotFoundError if location
# (even if it's a course, and we would otherwise immediately exit). # isn't found so we don't have to do it explicitly. Call this
parents = modulestore.get_parent_locations(loc) # 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) # print 'Processing loc={0}, path={1}'.format(loc, path)
if loc.category == "course": if loc.category == "course":
......
...@@ -23,12 +23,3 @@ def check_path_to_location(modulestore): ...@@ -23,12 +23,3 @@ def check_path_to_location(modulestore):
for location in not_found: for location in not_found:
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location) 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): ...@@ -280,14 +280,16 @@ class XMLModuleStore(ModuleStoreBase):
class_ = getattr(import_module(module_path), class_name) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ self.default_class = class_
self.parent_tracker = ParentTracker() self.parent_trackers = defaultdict(ParentTracker)
# If we are specifically asked for missing courses, that should # If we are specifically asked for missing courses, that should
# be an error. If we are asked for "all" courses, find the ones # 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: if course_dirs is None:
course_dirs = [d for d in os.listdir(self.data_dir) if course_dirs = sorted([d for d in os.listdir(self.data_dir) if
os.path.exists(self.data_dir / d / "course.xml")] os.path.exists(self.data_dir / d / "course.xml")])
for course_dir in course_dirs: for course_dir in course_dirs:
self.try_load_course(course_dir) self.try_load_course(course_dir)
...@@ -312,7 +314,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -312,7 +314,7 @@ class XMLModuleStore(ModuleStoreBase):
if course_descriptor is not None: if course_descriptor is not None:
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self._location_errors[course_descriptor.location] = errorlog 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: else:
# Didn't load course. Instead, save the errors elsewhere. # Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog self.errored_courses[course_dir] = errorlog
...@@ -415,7 +417,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -415,7 +417,7 @@ class XMLModuleStore(ModuleStoreBase):
course_dir, course_dir,
policy, policy,
tracker, tracker,
self.parent_tracker, self.parent_trackers[course_id],
self.load_error_modules, self.load_error_modules,
) )
...@@ -578,12 +580,15 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -578,12 +580,15 @@ class XMLModuleStore(ModuleStoreBase):
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
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 '''Find all locations that are the parents of this location in this
for path_to_location(). course. Needed for path_to_location().
returns an iterable of things that can be passed to Location. This may returns an iterable of things that can be passed to Location. This may
be empty if there are no parents. be empty if there are no parents.
''' '''
location = Location.ensure_fully_specified(location) 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)
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: ...@@ -23,7 +23,13 @@ Install the following:
### Databases ### 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 rake resetdb
...@@ -57,7 +63,11 @@ This runs all the tests (long, uses collectstatic): ...@@ -57,7 +63,11 @@ This runs all the tests (long, uses collectstatic):
If if you aren't changing static files, can run `rake test` once, then run 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: xmodule can be tested independently, with this:
......
...@@ -255,35 +255,5 @@ def get_courses(user, domain=None): ...@@ -255,35 +255,5 @@ def get_courses(user, domain=None):
courses = branding.get_visible_courses(domain) courses = branding.get_visible_courses(domain)
courses = [c for c in courses if has_access(user, c, 'see_exists')] 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) courses = sorted(courses, key=lambda course:course.number)
return courses 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): ...@@ -70,7 +70,7 @@ def courses(request):
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST')) courses = get_courses(request.user, domain=request.META.get('HTTP_HOST'))
# Sort courses by how far are they from they start day # 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) courses = sorted(courses, key=key, reverse=True)
return render_to_response("courseware/courses.html", {'courses': courses}) return render_to_response("courseware/courses.html", {'courses': courses})
...@@ -439,7 +439,7 @@ def university_profile(request, org_id): ...@@ -439,7 +439,7 @@ def university_profile(request, org_id):
domain=request.META.get('HTTP_HOST'))[org_id] domain=request.META.get('HTTP_HOST'))[org_id]
# Sort courses by how far are they from they start day # 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) courses = sorted(courses, key=key, reverse=True)
context = dict(courses=courses, org_id=org_id) context = dict(courses=courses, org_id=org_id)
......
...@@ -25,7 +25,6 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \ ...@@ -25,7 +25,6 @@ from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
from django_comment_client.utils import has_forum_access from django_comment_client.utils import has_forum_access
from instructor import staff_grading_service
from courseware.access import _course_staff_group_name from courseware.access import _course_staff_group_name
import courseware.tests.tests as ct import courseware.tests.tests as ct
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -101,7 +100,6 @@ def action_name(operation, rolename): ...@@ -101,7 +100,6 @@ def action_name(operation, rolename):
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[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) @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
class TestInstructorDashboardForumAdmin(ct.PageLoader): class TestInstructorDashboardForumAdmin(ct.PageLoader):
...@@ -224,94 +222,3 @@ 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)) 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 ...@@ -28,7 +28,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
from xmodule.modulestore.search import path_to_location from xmodule.modulestore.search import path_to_location
import track.views import track.views
from .grading import StaffGrading
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -414,26 +413,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ...@@ -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) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
......
# 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
...@@ -7,6 +7,8 @@ import logging ...@@ -7,6 +7,8 @@ import logging
import requests import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from grading_service import GradingService
from grading_service import GradingServiceError
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
...@@ -14,13 +16,11 @@ from django.http import HttpResponse, Http404 ...@@ -14,13 +16,11 @@ from django.http import HttpResponse, Http404
from courseware.access import has_access from courseware.access import has_access
from util.json_request import expect_json from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from student.models import unique_id_for_user
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class GradingServiceError(Exception):
pass
class MockStaffGradingService(object): class MockStaffGradingService(object):
""" """
...@@ -57,62 +57,16 @@ class MockStaffGradingService(object): ...@@ -57,62 +57,16 @@ class MockStaffGradingService(object):
return self.get_next(course_id, 'fake location', grader_id) return self.get_next(course_id, 'fake location', grader_id)
class StaffGradingService(object): class StaffGradingService(GradingService):
""" """
Interface to staff grading backend. Interface to staff grading backend.
""" """
def __init__(self, config): def __init__(self, config):
self.username = config['username'] super(StaffGradingService, self).__init__(config)
self.password = config['password']
self.url = config['url']
self.login_url = self.url + '/login/'
self.get_next_url = self.url + '/get_next_submission/' self.get_next_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/' self.save_grade_url = self.url + '/save_grade/'
self.get_problem_list_url = self.url + '/get_problem_list/' 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): def get_problem_list(self, course_id, grader_id):
""" """
...@@ -130,17 +84,8 @@ class StaffGradingService(object): ...@@ -130,17 +84,8 @@ class StaffGradingService(object):
Raises: Raises:
GradingServiceError: something went wrong with the connection. GradingServiceError: something went wrong with the connection.
""" """
op = lambda: self.session.get(self.get_problem_list_url, params = {'course_id': course_id,'grader_id': grader_id}
allow_redirects = False, return self.get(self.get_problem_list_url, params)
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
def get_next(self, course_id, location, grader_id): def get_next(self, course_id, location, grader_id):
...@@ -161,17 +106,9 @@ class StaffGradingService(object): ...@@ -161,17 +106,9 @@ class StaffGradingService(object):
Raises: Raises:
GradingServiceError: something went wrong with the connection. GradingServiceError: something went wrong with the connection.
""" """
op = lambda: self.session.get(self.get_next_url, return self.get(self.get_next_url,
allow_redirects=False,
params={'location': location, params={'location': location,
'grader_id': grader_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
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped):
...@@ -186,7 +123,6 @@ class StaffGradingService(object): ...@@ -186,7 +123,6 @@ class StaffGradingService(object):
Raises: Raises:
GradingServiceError if there's a problem connecting. GradingServiceError if there's a problem connecting.
""" """
try:
data = {'course_id': course_id, data = {'course_id': course_id,
'submission_id': submission_id, 'submission_id': submission_id,
'score': score, 'score': score,
...@@ -194,20 +130,13 @@ class StaffGradingService(object): ...@@ -194,20 +130,13 @@ class StaffGradingService(object):
'grader_id': grader_id, 'grader_id': grader_id,
'skipped': skipped} 'skipped': skipped}
op = lambda: self.session.post(self.save_grade_url, data=data, return self.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 r.text # don't initialize until staff_grading_service() is called--means that just
# don't initialize until grading_service() is called--means that just
# importing this file doesn't create objects that may not have the right config # importing this file doesn't create objects that may not have the right config
_service = None _service = None
def grading_service(): def staff_grading_service():
""" """
Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True, Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
returns a mock one, otherwise a real one. returns a mock one, otherwise a real one.
...@@ -248,7 +177,7 @@ def _check_access(user, course_id): ...@@ -248,7 +177,7 @@ def _check_access(user, course_id):
def get_next(request, course_id): def get_next(request, course_id):
""" """
Get the next thing to grade for course_id and with the location specified 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: Returns a json dict with the following keys:
...@@ -276,11 +205,11 @@ def get_next(request, course_id): ...@@ -276,11 +205,11 @@ def get_next(request, course_id):
if len(missing) > 0: if len(missing) > 0:
return _err_response('Missing required keys {0}'.format( return _err_response('Missing required keys {0}'.format(
', '.join(missing))) ', '.join(missing)))
grader_id = request.user.id grader_id = unique_id_for_user(request.user)
p = request.POST p = request.POST
location = p['location'] 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") mimetype="application/json")
...@@ -308,12 +237,12 @@ def get_problem_list(request, course_id): ...@@ -308,12 +237,12 @@ def get_problem_list(request, course_id):
""" """
_check_access(request.user, course_id) _check_access(request.user, course_id)
try: 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, return HttpResponse(response,
mimetype="application/json") mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}" log.exception("Error from grading service. server url: {0}"
.format(grading_service().url)) .format(staff_grading_service().url))
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'Could not connect to grading service'})) 'error': 'Could not connect to grading service'}))
...@@ -323,10 +252,10 @@ def _get_next(course_id, grader_id, location): ...@@ -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 Implementation of get_next (also called from save_grade) -- returns a json string
""" """
try: try:
return grading_service().get_next(course_id, location, grader_id) return staff_grading_service().get_next(course_id, location, grader_id)
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}" log.exception("Error from grading service. server url: {0}"
.format(grading_service().url)) .format(staff_grading_service().url))
return json.dumps({'success': False, return json.dumps({'success': False,
'error': 'Could not connect to grading service'}) 'error': 'Could not connect to grading service'})
...@@ -357,14 +286,14 @@ def save_grade(request, course_id): ...@@ -357,14 +286,14 @@ def save_grade(request, course_id):
return _err_response('Missing required keys {0}'.format( return _err_response('Missing required keys {0}'.format(
', '.join(missing))) ', '.join(missing)))
grader_id = request.user.id grader_id = unique_id_for_user(request.user)
p = request.POST p = request.POST
location = p['location'] location = p['location']
skipped = 'skipped' in p skipped = 'skipped' in p
try: try:
result_json = grading_service().save_grade(course_id, result_json = staff_grading_service().save_grade(course_id,
grader_id, grader_id,
p['submission_id'], p['submission_id'],
p['score'], p['score'],
......
"""
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'])
# 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 ...@@ -332,6 +332,9 @@ STAFF_GRADING_INTERFACE = None
# Used for testing, debugging # Used for testing, debugging
MOCK_STAFF_GRADING = False MOCK_STAFF_GRADING = False
################################# Peer grading config #####################
PEER_GRADING_INTERFACE = None
MOCK_PEER_GRADING = False
################################# Jasmine ################################### ################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
...@@ -410,9 +413,8 @@ main_vendor_js = [ ...@@ -410,9 +413,8 @@ main_vendor_js = [
] ]
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee'))
PIPELINE_CSS = { PIPELINE_CSS = {
'application': { 'application': {
...@@ -438,11 +440,12 @@ PIPELINE_CSS = { ...@@ -438,11 +440,12 @@ PIPELINE_CSS = {
PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/course.scss'] PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/course.scss']
PIPELINE_JS = { PIPELINE_JS = {
'application': { 'application': {
# Application will contain all paths not in courseware_only_js # Application will contain all paths not in courseware_only_js
'source_filenames': sorted( 'source_filenames': sorted(
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
set(courseware_js + discussion_js + staff_grading_js) set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
) + [ ) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
...@@ -471,8 +474,11 @@ PIPELINE_JS = { ...@@ -471,8 +474,11 @@ PIPELINE_JS = {
'staff_grading' : { 'staff_grading' : {
'source_filenames': staff_grading_js, 'source_filenames': staff_grading_js,
'output_filename': 'js/staff_grading.js' 'output_filename': 'js/staff_grading.js'
},
'peer_grading' : {
'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js'
} }
} }
PIPELINE_DISABLE_WRAPPER = True PIPELINE_DISABLE_WRAPPER = True
...@@ -545,6 +551,7 @@ INSTALLED_APPS = ( ...@@ -545,6 +551,7 @@ INSTALLED_APPS = (
'util', 'util',
'certificates', 'certificates',
'instructor', 'instructor',
'open_ended_grading',
'psychometrics', 'psychometrics',
'licenses', 'licenses',
......
...@@ -114,6 +114,13 @@ STAFF_GRADING_INTERFACE = { ...@@ -114,6 +114,13 @@ STAFF_GRADING_INTERFACE = {
'password': 'abcd', 'password': 'abcd',
} }
################################# Peer grading config #####################
PEER_GRADING_INTERFACE = {
'url': 'http://127.0.0.1:3033/peer_grading',
'username': 'lms',
'password': 'abcd',
}
################################ LMS Migration ################################# ################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True 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 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 ...@@ -62,6 +62,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
# Don't rely on a real staff grading backend # Don't rely on a real staff grading backend
MOCK_STAFF_GRADING = True MOCK_STAFF_GRADING = True
MOCK_PEER_GRADING = True
# TODO (cpennington): We need to figure out how envs/test.py can inject things # 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 # into common.py so that we don't have to repeat this sort of thing
......
...@@ -422,7 +422,8 @@ class formula(object): ...@@ -422,7 +422,8 @@ class formula(object):
def GetContentMathML(self, asciimath, mathml): def GetContentMathML(self, asciimath, mathml):
# URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo' # 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: if 1:
payload = {'asciiMathInput': asciimath, payload = {'asciiMathInput': asciimath,
...@@ -430,7 +431,7 @@ class formula(object): ...@@ -430,7 +431,7 @@ class formula(object):
#'asciiMathML':unicode(mathml).encode('utf-8'), #'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"} 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' r.encoding = 'utf-8'
ret = r.text ret = r.text
#print "encoding: ",r.encoding #print "encoding: ",r.encoding
......
# 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())
div.staff-grading { div.staff-grading,
div.peer-grading{
textarea.feedback-area { textarea.feedback-area {
height: 75px; height: 75px;
margin: 20px; margin: 20px;
...@@ -36,8 +37,8 @@ div.staff-grading { ...@@ -36,8 +37,8 @@ div.staff-grading {
} }
.prompt-information-container, .prompt-information-container,
.submission-wrapper,
.rubric-wrapper, .rubric-wrapper,
.calibration-feedback-wrapper,
.grading-container .grading-container
{ {
border: 1px solid gray; border: 1px solid gray;
...@@ -49,6 +50,18 @@ div.staff-grading { ...@@ -49,6 +50,18 @@ div.staff-grading {
padding: 15px; padding: 15px;
margin-left: 0px; margin-left: 0px;
} }
.submission-wrapper
{
h3
{
margin-bottom: 15px;
}
p
{
margin-left:10px;
}
padding: 15px;
}
.meta-info-wrapper .meta-info-wrapper
{ {
background-color: #eee; background-color: #eee;
...@@ -67,7 +80,8 @@ div.staff-grading { ...@@ -67,7 +80,8 @@ div.staff-grading {
} }
} }
} }
.message-container .message-container,
.grading-message
{ {
background-color: $yellow; background-color: $yellow;
padding: 10px; padding: 10px;
...@@ -82,5 +96,68 @@ div.staff-grading { ...@@ -82,5 +96,68 @@ div.staff-grading {
font-size: .8em; 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; padding: 40px;
} }
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%> %>
<%page args="course" /> <%page args="course" />
<article id="${course.id}" class="course"> <article id="${course.id}" class="course">
%if course.metadata.get('is_new'): %if course.is_new:
<span class="status">New</span> <span class="status">New</span>
%endif %endif
<a href="${reverse('about_course', args=[course.id])}"> <a href="${reverse('about_course', args=[course.id])}">
......
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
</div> </div>
<div class="message-container"> <div class="message-container">
</div> </div>
<! -- Problem List View -->
<section class="problem-list-container"> <section class="problem-list-container">
<h2>Instructions</h2> <h2>Instructions</h2>
<div class="instructions"> <div class="instructions">
...@@ -35,6 +37,8 @@ ...@@ -35,6 +37,8 @@
</ul> </ul>
</section> </section>
<!-- Grading View -->
<section class="prompt-wrapper"> <section class="prompt-wrapper">
<h2 class="prompt-name"></h2> <h2 class="prompt-name"></h2>
<div class="meta-info-wrapper"> <div class="meta-info-wrapper">
......
<%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>
<%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 @@ ...@@ -21,7 +21,7 @@
<article class="response"> <article class="response">
<h3>What is edX?</h3> <h3>What is edX?</h3>
<p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p> <p>edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at <a href="http://www.edx.org">www.edx.org</a> for online education.</p>
<p>EdX currently offers HarvardX, <em>MITx</em> and BerkeleyX classes online for free. Beginning in fall 2013, edX will offer WellesleyX and GeorgetownX classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.</p> <p>EdX currently offers HarvardX, <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>
<article class="response"> <article class="response">
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3>EdX is looking to add new talent to our team! </h3> <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 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.&nbsp; 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.&nbsp; 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>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&rsquo;ll receive:</p> <p>As part of the edX team, you&rsquo;ll receive:</p>
<ul> <ul>
......
...@@ -243,15 +243,32 @@ if settings.COURSEWARE_ENABLED: ...@@ -243,15 +243,32 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
'instructor.views.enroll_students', name='enroll_students'), 'instructor.views.enroll_students', name='enroll_students'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading$', 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$', 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$', 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$', 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$', 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 # discussion forums live within courseware, so courseware must be enabled first
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment