Commit ef483650 by cewing

MIT CCX: Use CCX Keys - responses to code review

remove references to middleware that were missed previously

use key apis rather than local implementation of key conversion.  remove local implementationa

remove spurious test for attribute

fix test setUp to avoid unneeded flattening

code quality fixes

add security check ensuring that the coach is coach for *this* CCX.

prevent ccx/deprecated course id problems

1.  do not allow ccx objects to be created if the course id is deprecated
2.  filter out any ccx memberships that involve deprecated course ids (in case there are bad ccxs in the database)

Fix test failures and errors arising from incorrect code path execution

Create context manager to handle unwrapping and restoring ccx values for the modulestore wrapper, employ it throughout modulestore wrapper implementation
parent 6a0c9aee
...@@ -48,23 +48,17 @@ class TestCCXModulestoreWrapper(ModuleStoreTestCase): ...@@ -48,23 +48,17 @@ class TestCCXModulestoreWrapper(ModuleStoreTestCase):
2010, 7, 7, 0, 0, tzinfo=pytz.UTC) 2010, 7, 7, 0, 0, tzinfo=pytz.UTC)
chapters = [ItemFactory.create(start=start, parent=course) chapters = [ItemFactory.create(start=start, parent=course)
for _ in xrange(2)] for _ in xrange(2)]
sequentials = flatten([ sequentials = [
[ ItemFactory.create(parent=c) for _ in xrange(2) for c in chapters
ItemFactory.create(parent=chapter) for _ in xrange(2) ]
] for chapter in chapters verticals = [
]) ItemFactory.create(
verticals = flatten([ due=due, parent=s, graded=True, format='Homework'
[ ) for _ in xrange(2) for s in sequentials
ItemFactory.create( ]
due=due, parent=sequential, graded=True, format='Homework' blocks = [
) for _ in xrange(2) ItemFactory.create(parent=v) for _ in xrange(2) for v in verticals
] for sequential in sequentials ]
])
blocks = flatten([ # pylint: disable=unused-variable
[
ItemFactory.create(parent=vertical) for _ in xrange(2)
] for vertical in verticals
])
self.ccx = ccx = CustomCourseForEdX( self.ccx = ccx = CustomCourseForEdX(
course_id=course.id, course_id=course.id,
......
...@@ -69,9 +69,9 @@ class TestEmailEnrollmentState(ModuleStoreTestCase): ...@@ -69,9 +69,9 @@ class TestEmailEnrollmentState(ModuleStoreTestCase):
"""verify behavior for non-user email address """verify behavior for non-user email address
""" """
ee_state = self.create_one(email='nobody@nowhere.com') ee_state = self.create_one(email='nobody@nowhere.com')
for attr in ['user', 'member', 'full_name', 'in_ccx']: for attribute in ['user', 'member', 'full_name', 'in_ccx']:
value = getattr(ee_state, attr, 'missing attribute') value = getattr(ee_state, attribute, 'missing attribute')
self.assertFalse(value, "{}: {}".format(value, attr)) self.assertFalse(value, "{}: {}".format(value, attribute))
def test_enrollment_state_for_non_member_user(self): def test_enrollment_state_for_non_member_user(self):
"""verify behavior for email address of user who is not a ccx memeber """verify behavior for email address of user who is not a ccx memeber
...@@ -89,10 +89,10 @@ class TestEmailEnrollmentState(ModuleStoreTestCase): ...@@ -89,10 +89,10 @@ class TestEmailEnrollmentState(ModuleStoreTestCase):
self.create_user() self.create_user()
self.register_user_in_ccx() self.register_user_in_ccx()
ee_state = self.create_one() ee_state = self.create_one()
for attr in ['user', 'in_ccx']: for attribute in ['user', 'in_ccx']:
self.assertTrue( self.assertTrue(
getattr(ee_state, attr, False), getattr(ee_state, attribute, False),
"attribute {} is missing or False".format(attr) "attribute {} is missing or False".format(attribute)
) )
self.assertEqual(ee_state.member, self.user) self.assertEqual(ee_state.member, self.user)
self.assertEqual(ee_state.full_name, self.user.profile.name) self.assertEqual(ee_state.full_name, self.user.profile.name)
......
...@@ -262,6 +262,17 @@ def get_ccx_membership_triplets(user, course_org_filter, org_filter_out_set): ...@@ -262,6 +262,17 @@ def get_ccx_membership_triplets(user, course_org_filter, org_filter_out_set):
elif course.location.org in org_filter_out_set: elif course.location.org in org_filter_out_set:
continue continue
# If, somehow, we've got a ccx that has been created for a
# course with a deprecated ID, we must filter it out. Emit a
# warning to the log so we can clean up.
if course.location.deprecated:
log.warning(
"CCX {} exists for course {} with deprecated id".format(
ccx, ccx.course_id
)
)
continue
yield (ccx, membership, course) yield (ccx, membership, course)
else: else:
log.error("User {0} enrolled in {2} course {1}".format( # pylint: disable=logging-format-interpolation log.error("User {0} enrolled in {2} course {1}".format( # pylint: disable=logging-format-interpolation
......
...@@ -18,6 +18,7 @@ from django.http import ( ...@@ -18,6 +18,7 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
HttpResponseRedirect, HttpResponseRedirect,
) )
from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.http import Http404 from django.http import Http404
...@@ -74,8 +75,6 @@ def coach_dashboard(view): ...@@ -74,8 +75,6 @@ def coach_dashboard(view):
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
ccx = None ccx = None
if isinstance(course_key, CCXLocator): if isinstance(course_key, CCXLocator):
# is there a security leak here in not checking that this user is
# the coach for this ccx?
ccx_id = course_key.ccx ccx_id = course_key.ccx
ccx = CustomCourseForEdX.objects.get(pk=ccx_id) ccx = CustomCourseForEdX.objects.get(pk=ccx_id)
course_key = ccx.course_id course_key = ccx.course_id
...@@ -86,6 +85,15 @@ def coach_dashboard(view): ...@@ -86,6 +85,15 @@ def coach_dashboard(view):
_('You must be a CCX Coach to access this view.')) _('You must be a CCX Coach to access this view.'))
course = get_course_by_id(course_key, depth=None) course = get_course_by_id(course_key, depth=None)
# if there is a ccx, we must validate that it is the ccx for this coach
if ccx is not None:
coach_ccx = get_ccx_for_coach(course, request.user)
if coach_ccx is None or coach_ccx.id != ccx.id:
return HttpResponseForbidden(
_('You must be the coach for this ccx to access this view')
)
return view(request, course, ccx) return view(request, course, ccx)
return wrapper return wrapper
...@@ -140,6 +148,17 @@ def create_ccx(request, course, ccx=None): ...@@ -140,6 +148,17 @@ def create_ccx(request, course, ccx=None):
Create a new CCX Create a new CCX
""" """
name = request.POST.get('name') name = request.POST.get('name')
# prevent CCX objects from being created for deprecated course ids.
if course.id.deprecated:
messages.error(_(
"You cannot create a CCX from a course using a deprecated id. "
"Please create a rerun of this course in the studio to allow "
"this action.")
)
url = reverse('ccx_coach_dashboard', kwargs={'course_id', course.id})
return redirect(url)
ccx = CustomCourseForEdX( ccx = CustomCourseForEdX(
course_id=course.id, course_id=course.id,
coach=request.user, coach=request.user,
......
...@@ -110,9 +110,10 @@ def has_access(user, action, obj, course_key=None): ...@@ -110,9 +110,10 @@ def has_access(user, action, obj, course_key=None):
if isinstance(obj, XBlock): if isinstance(obj, XBlock):
return _has_access_descriptor(user, action, obj, course_key) return _has_access_descriptor(user, action, obj, course_key)
if isinstance(obj, CCXLocator):
return _has_access_ccx_key(user, action, obj)
if isinstance(obj, CourseKey): if isinstance(obj, CourseKey):
if isinstance(obj, CCXLocator):
obj = obj.to_course_locator()
return _has_access_course_key(user, action, obj) return _has_access_course_key(user, action, obj)
if isinstance(obj, UsageKey): if isinstance(obj, UsageKey):
...@@ -493,6 +494,10 @@ def _has_access_course_key(user, action, course_key): ...@@ -493,6 +494,10 @@ def _has_access_course_key(user, action, course_key):
return _dispatch(checkers, action, user, course_key) return _dispatch(checkers, action, user, course_key)
def _has_access_ccx_key(user, action, ccx_key):
course_key = ccx_key.to_course_locator()
return _has_access_course_key(user, action, course_key)
def _has_access_string(user, action, perm): def _has_access_string(user, action, perm):
""" """
......
...@@ -43,6 +43,7 @@ from xmodule.modulestore import ModuleStoreEnum ...@@ -43,6 +43,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from ccx.modulestore import CCXModulestoreWrapper
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -59,9 +60,13 @@ class SysadminDashboardView(TemplateView): ...@@ -59,9 +60,13 @@ class SysadminDashboardView(TemplateView):
modulestore_type and return msg modulestore_type and return msg
""" """
self.def_ms = modulestore() self.def_ms = check_ms = modulestore()
# if the modulestore is wrapped by CCX, unwrap it for checking purposes
if isinstance(check_ms, CCXModulestoreWrapper):
check_ms = check_ms._modulestore
self.is_using_mongo = True self.is_using_mongo = True
if isinstance(self.def_ms, XMLModuleStore): if isinstance(check_ms, XMLModuleStore):
self.is_using_mongo = False self.is_using_mongo = False
self.msg = u'' self.msg = u''
self.datatable = [] self.datatable = []
......
...@@ -33,6 +33,7 @@ from xmodule.modulestore.django import modulestore ...@@ -33,6 +33,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from ccx.modulestore import CCXModulestoreWrapper
TEST_MONGODB_LOG = { TEST_MONGODB_LOG = {
...@@ -315,8 +316,12 @@ class TestSysadmin(SysadminBaseTestCase): ...@@ -315,8 +316,12 @@ class TestSysadmin(SysadminBaseTestCase):
# Create git loaded course # Create git loaded course
response = self._add_edx4edx() response = self._add_edx4edx()
def_ms = modulestore() def_ms = check_ms = modulestore()
self.assertIn('xml', str(def_ms.__class__)) # if the modulestore is wrapped by CCX, unwrap it for testing purposes
if isinstance(check_ms, CCXModulestoreWrapper):
check_ms = check_ms._modulestore
self.assertIn('xml', str(check_ms.__class__))
course = def_ms.courses.get('{0}/edx4edx_lite'.format( course = def_ms.courses.get('{0}/edx4edx_lite'.format(
os.path.abspath(settings.DATA_DIR)), None) os.path.abspath(settings.DATA_DIR)), None)
self.assertIsNotNone(course) self.assertIsNotNone(course)
......
...@@ -612,7 +612,6 @@ ECOMMERCE_API_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_API_TIMEOUT', ECOMMERCE_API_TI ...@@ -612,7 +612,6 @@ ECOMMERCE_API_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_API_TIMEOUT', ECOMMERCE_API_TI
##### Custom Courses for EdX ##### ##### Custom Courses for EdX #####
if FEATURES.get('CUSTOM_COURSES_EDX'): if FEATURES.get('CUSTOM_COURSES_EDX'):
INSTALLED_APPS += ('ccx',) INSTALLED_APPS += ('ccx',)
MIDDLEWARE_CLASSES += ('ccx.overrides.CcxMiddleware',)
FIELD_OVERRIDE_PROVIDERS += ( FIELD_OVERRIDE_PROVIDERS += (
'ccx.overrides.CustomCoursesForEdxOverrideProvider', 'ccx.overrides.CustomCoursesForEdxOverrideProvider',
) )
......
...@@ -307,7 +307,6 @@ GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE ...@@ -307,7 +307,6 @@ GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE
##### Custom Courses for EdX ##### ##### Custom Courses for EdX #####
if FEATURES.get('CUSTOM_COURSES_EDX'): if FEATURES.get('CUSTOM_COURSES_EDX'):
INSTALLED_APPS += ('ccx',) INSTALLED_APPS += ('ccx',)
MIDDLEWARE_CLASSES += ('ccx.overrides.CcxMiddleware',)
FIELD_OVERRIDE_PROVIDERS += ( FIELD_OVERRIDE_PROVIDERS += (
'ccx.overrides.CustomCoursesForEdxOverrideProvider', 'ccx.overrides.CustomCoursesForEdxOverrideProvider',
) )
......
...@@ -22,6 +22,18 @@ from django.core.urlresolvers import reverse ...@@ -22,6 +22,18 @@ from django.core.urlresolvers import reverse
<section class="instructor-dashboard-content-2" id="ccx-coach-dashboard-content"> <section class="instructor-dashboard-content-2" id="ccx-coach-dashboard-content">
<h1>${_("CCX Coach Dashboard")}</h1> <h1>${_("CCX Coach Dashboard")}</h1>
% if messages:
<ul class="messages">
% for message in messages:
% if message.tags:
<li class="${message.tags}">${message}</li>
% else:
<li>${message}</li>
% endif
% endfor
</ul>
% endif
%if not ccx: %if not ccx:
<section> <section>
<form action="${create_ccx_url}" method="POST"> <form action="${create_ccx_url}" method="POST">
......
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