923 lines
36 KiB
Python
923 lines
36 KiB
Python
import logging
|
|
log = logging.getLogger("mitx." + __name__)
|
|
|
|
import json
|
|
import time
|
|
|
|
from urlparse import urlsplit, urlunsplit
|
|
|
|
from django.contrib.auth.models import User, Group
|
|
from django.test import TestCase
|
|
from django.test.client import RequestFactory
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.test.utils import override_settings
|
|
|
|
import xmodule.modulestore.django
|
|
from xmodule.modulestore.mongo import MongoModuleStore
|
|
|
|
|
|
# Need access to internal func to put users in the right group
|
|
from courseware import grades
|
|
from courseware.access import (has_access, _course_staff_group_name,
|
|
course_beta_test_group_name)
|
|
from courseware.models import StudentModuleCache
|
|
|
|
from student.models import Registration
|
|
from xmodule.error_module import ErrorDescriptor
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore import Location
|
|
from xmodule.modulestore.xml_importer import import_from_xml
|
|
from xmodule.modulestore.xml import XMLModuleStore
|
|
from xmodule.timeparse import stringify_time
|
|
|
|
|
|
def parse_json(response):
|
|
"""Parse response, which is assumed to be json"""
|
|
return json.loads(response.content)
|
|
|
|
|
|
def user(email):
|
|
'''look up a user by email'''
|
|
return User.objects.get(email=email)
|
|
|
|
|
|
def registration(email):
|
|
'''look up registration object by email'''
|
|
return Registration.objects.get(user__email=email)
|
|
|
|
# A bit of a hack--want mongo modulestore for these tests, until
|
|
# jump_to works with the xmlmodulestore or we have an even better solution
|
|
# NOTE: this means this test requires mongo to be running.
|
|
|
|
|
|
def mongo_store_config(data_dir):
|
|
return {
|
|
'default': {
|
|
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
|
'OPTIONS': {
|
|
'default_class': 'xmodule.raw_module.RawDescriptor',
|
|
'host': 'localhost',
|
|
'db': 'test_xmodule',
|
|
'collection': 'modulestore',
|
|
'fs_root': data_dir,
|
|
'render_template': 'mitxmako.shortcuts.render_to_string',
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def draft_mongo_store_config(data_dir):
|
|
return {
|
|
'default': {
|
|
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
|
'OPTIONS': {
|
|
'default_class': 'xmodule.raw_module.RawDescriptor',
|
|
'host': 'localhost',
|
|
'db': 'test_xmodule',
|
|
'collection': 'modulestore',
|
|
'fs_root': data_dir,
|
|
'render_template': 'mitxmako.shortcuts.render_to_string',
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def xml_store_config(data_dir):
|
|
return {
|
|
'default': {
|
|
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
|
'OPTIONS': {
|
|
'data_dir': data_dir,
|
|
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
|
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
|
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
|
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
|
|
|
|
|
|
class ActivateLoginTestCase(TestCase):
|
|
'''Check that we can activate and log in'''
|
|
|
|
def assertRedirectsNoFollow(self, response, expected_url):
|
|
"""
|
|
http://devblog.point2.com/2010/04/23/djangos-assertredirects-little-gotcha/
|
|
|
|
Don't check that the redirected-to page loads--there should be other tests for that.
|
|
|
|
Some of the code taken from django.test.testcases.py
|
|
"""
|
|
self.assertEqual(response.status_code, 302,
|
|
'Response status code was {0} instead of 302'.format(response.status_code))
|
|
url = response['Location']
|
|
|
|
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(
|
|
expected_url)
|
|
if not (e_scheme or e_netloc):
|
|
expected_url = urlunsplit(('http', 'testserver', e_path,
|
|
e_query, e_fragment))
|
|
|
|
self.assertEqual(url, expected_url, "Response redirected to '{0}', expected '{1}'".format(
|
|
url, expected_url))
|
|
|
|
def setUp(self):
|
|
email = 'view@test.com'
|
|
password = 'foo'
|
|
self.create_account('viewtest', email, password)
|
|
self.activate_user(email)
|
|
self.login(email, password)
|
|
|
|
# ============ User creation and login ==============
|
|
|
|
def _login(self, email, pw):
|
|
'''Login. View should always return 200. The success/fail is in the
|
|
returned json'''
|
|
resp = self.client.post(reverse('login'),
|
|
{'email': email, 'password': pw})
|
|
self.assertEqual(resp.status_code, 200)
|
|
return resp
|
|
|
|
def login(self, email, pw):
|
|
'''Login, check that it worked.'''
|
|
resp = self._login(email, pw)
|
|
data = parse_json(resp)
|
|
self.assertTrue(data['success'])
|
|
return resp
|
|
|
|
def logout(self):
|
|
'''Logout, check that it worked.'''
|
|
resp = self.client.get(reverse('logout'), {})
|
|
# should redirect
|
|
self.assertEqual(resp.status_code, 302)
|
|
return resp
|
|
|
|
def _create_account(self, username, email, pw):
|
|
'''Try to create an account. No error checking'''
|
|
resp = self.client.post('/create_account', {
|
|
'username': username,
|
|
'email': email,
|
|
'password': pw,
|
|
'name': 'Fred Weasley',
|
|
'terms_of_service': 'true',
|
|
'honor_code': 'true',
|
|
})
|
|
return resp
|
|
|
|
def create_account(self, username, email, pw):
|
|
'''Create the account and check that it worked'''
|
|
resp = self._create_account(username, email, pw)
|
|
self.assertEqual(resp.status_code, 200)
|
|
data = parse_json(resp)
|
|
self.assertEqual(data['success'], True)
|
|
|
|
# Check both that the user is created, and inactive
|
|
self.assertFalse(user(email).is_active)
|
|
|
|
return resp
|
|
|
|
def _activate_user(self, email):
|
|
'''Look up the activation key for the user, then hit the activate view.
|
|
No error checking'''
|
|
activation_key = registration(email).activation_key
|
|
|
|
# and now we try to activate
|
|
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
|
return resp
|
|
|
|
def activate_user(self, email):
|
|
resp = self._activate_user(email)
|
|
self.assertEqual(resp.status_code, 200)
|
|
# Now make sure that the user is now actually activated
|
|
self.assertTrue(user(email).is_active)
|
|
|
|
def test_activate_login(self):
|
|
'''The setup function does all the work'''
|
|
pass
|
|
|
|
def test_logout(self):
|
|
'''Setup function does login'''
|
|
self.logout()
|
|
|
|
|
|
class PageLoader(ActivateLoginTestCase):
|
|
''' Base class that adds a function to load all pages in a modulestore '''
|
|
|
|
def _enroll(self, course):
|
|
"""Post to the enrollment view, and return the parsed json response"""
|
|
resp = self.client.post('/change_enrollment', {
|
|
'enrollment_action': 'enroll',
|
|
'course_id': course.id,
|
|
})
|
|
return parse_json(resp)
|
|
|
|
def try_enroll(self, course):
|
|
"""Try to enroll. Return bool success instead of asserting it."""
|
|
data = self._enroll(course)
|
|
print 'Enrollment in {0} result: {1}'.format(course.location.url(), data)
|
|
return data['success']
|
|
|
|
def enroll(self, course):
|
|
"""Enroll the currently logged-in user, and check that it worked."""
|
|
data = self._enroll(course)
|
|
self.assertTrue(data['success'])
|
|
|
|
def unenroll(self, course):
|
|
"""Unenroll the currently logged-in user, and check that it worked."""
|
|
resp = self.client.post('/change_enrollment', {
|
|
'enrollment_action': 'unenroll',
|
|
'course_id': course.id,
|
|
})
|
|
data = parse_json(resp)
|
|
self.assertTrue(data['success'])
|
|
|
|
|
|
def check_for_get_code(self, code, url):
|
|
"""
|
|
Check that we got the expected code when accessing url via GET.
|
|
Returns the response.
|
|
"""
|
|
resp = self.client.get(url)
|
|
self.assertEqual(resp.status_code, code,
|
|
"got code {0} for url '{1}'. Expected code {2}"
|
|
.format(resp.status_code, url, code))
|
|
return resp
|
|
|
|
|
|
def check_for_post_code(self, code, url, data={}):
|
|
"""
|
|
Check that we got the expected code when accessing url via POST.
|
|
Returns the response.
|
|
"""
|
|
resp = self.client.post(url, data)
|
|
self.assertEqual(resp.status_code, code,
|
|
"got code {0} for url '{1}'. Expected code {2}"
|
|
.format(resp.status_code, url, code))
|
|
return resp
|
|
|
|
|
|
|
|
def check_pages_load(self, module_store):
|
|
"""Make all locations in course load"""
|
|
|
|
|
|
# enroll in the course before trying to access pages
|
|
courses = module_store.get_courses()
|
|
self.assertEqual(len(courses), 1)
|
|
course = courses[0]
|
|
self.enroll(course)
|
|
course_id = course.id
|
|
|
|
n = 0
|
|
num_bad = 0
|
|
all_ok = True
|
|
|
|
for descriptor in module_store.get_items(
|
|
Location(None, None, None, None, None)):
|
|
|
|
n += 1
|
|
print "Checking ", descriptor.location.url()
|
|
|
|
# We have ancillary course information now as modules and we can't simply use 'jump_to' to view them
|
|
if descriptor.location.category == 'about':
|
|
resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id}))
|
|
msg = str(resp.status_code)
|
|
|
|
if resp.status_code != 200:
|
|
msg = "ERROR " + msg
|
|
all_ok = False
|
|
num_bad += 1
|
|
elif descriptor.location.category == 'static_tab':
|
|
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug': descriptor.location.name}))
|
|
msg = str(resp.status_code)
|
|
|
|
if resp.status_code != 200:
|
|
msg = "ERROR " + msg
|
|
all_ok = False
|
|
num_bad += 1
|
|
elif descriptor.location.category == 'course_info':
|
|
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
|
|
msg = str(resp.status_code)
|
|
|
|
if resp.status_code != 200:
|
|
msg = "ERROR " + msg
|
|
all_ok = False
|
|
num_bad += 1
|
|
elif descriptor.location.category == 'custom_tag_template':
|
|
pass
|
|
else:
|
|
#print descriptor.__class__, descriptor.location
|
|
resp = self.client.get(reverse('jump_to',
|
|
kwargs={'course_id': course_id,
|
|
'location': descriptor.location.url()}), follow=True)
|
|
msg = str(resp.status_code)
|
|
|
|
if resp.status_code != 200:
|
|
msg = "ERROR " + msg + ": " + descriptor.location.url()
|
|
all_ok = False
|
|
num_bad += 1
|
|
elif resp.redirect_chain[0][1] != 302:
|
|
msg = "ERROR on redirect from " + descriptor.location.url()
|
|
all_ok = False
|
|
num_bad += 1
|
|
|
|
# check content to make sure there were no rendering failures
|
|
content = resp.content
|
|
if content.find("this module is temporarily unavailable") >= 0:
|
|
msg = "ERROR unavailable module "
|
|
all_ok = False
|
|
num_bad += 1
|
|
elif isinstance(descriptor, ErrorDescriptor):
|
|
msg = "ERROR error descriptor loaded: "
|
|
msg = msg + descriptor.definition['data']['error_msg']
|
|
all_ok = False
|
|
num_bad += 1
|
|
|
|
print msg
|
|
self.assertTrue(all_ok) # fail fast
|
|
|
|
print "{0}/{1} good".format(n - num_bad, n)
|
|
log.info("{0}/{1} good".format(n - num_bad, n))
|
|
self.assertTrue(all_ok)
|
|
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
|
class TestCoursesLoadTestCase_XmlModulestore(PageLoader):
|
|
'''Check that all pages in test courses load properly'''
|
|
|
|
def setUp(self):
|
|
ActivateLoginTestCase.setUp(self)
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
|
|
def test_toy_course_loads(self):
|
|
module_store = XMLModuleStore(
|
|
TEST_DATA_DIR,
|
|
default_class='xmodule.hidden_module.HiddenDescriptor',
|
|
course_dirs=['toy'],
|
|
load_error_modules=True,
|
|
)
|
|
|
|
self.check_pages_load(module_store)
|
|
|
|
def test_full_course_loads(self):
|
|
module_store = XMLModuleStore(
|
|
TEST_DATA_DIR,
|
|
default_class='xmodule.hidden_module.HiddenDescriptor',
|
|
course_dirs=['full'],
|
|
load_error_modules=True,
|
|
)
|
|
self.check_pages_load(module_store)
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
|
class TestCoursesLoadTestCase_MongoModulestore(PageLoader):
|
|
'''Check that all pages in test courses load properly'''
|
|
|
|
def setUp(self):
|
|
ActivateLoginTestCase.setUp(self)
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
modulestore().collection.drop()
|
|
|
|
def test_toy_course_loads(self):
|
|
module_store = modulestore()
|
|
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
|
|
self.check_pages_load(module_store)
|
|
|
|
def test_full_course_loads(self):
|
|
module_store = modulestore()
|
|
import_from_xml(module_store, TEST_DATA_DIR, ['full'])
|
|
self.check_pages_load(module_store)
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
|
class TestNavigation(PageLoader):
|
|
"""Check that navigation state is saved properly"""
|
|
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
|
|
# Assume courses are there
|
|
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
|
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
|
|
|
# Create two accounts
|
|
self.student = 'view@test.com'
|
|
self.student2 = 'view2@test.com'
|
|
self.password = 'foo'
|
|
self.create_account('u1', self.student, self.password)
|
|
self.create_account('u2', self.student2, self.password)
|
|
self.activate_user(self.student)
|
|
self.activate_user(self.student2)
|
|
|
|
def test_accordion_state(self):
|
|
"""Make sure that the accordion remembers where you were properly"""
|
|
self.login(self.student, self.password)
|
|
self.enroll(self.toy)
|
|
self.enroll(self.full)
|
|
|
|
# First request should redirect to ToyVideos
|
|
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
|
|
|
# Don't use no-follow, because state should only be saved once we actually hit the section
|
|
self.assertRedirects(resp, reverse(
|
|
'courseware_section', kwargs={'course_id': self.toy.id,
|
|
'chapter': 'Overview',
|
|
'section': 'Toy_Videos'}))
|
|
|
|
# Hitting the couseware tab again should redirect to the first chapter: 'Overview'
|
|
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
|
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
|
kwargs={'course_id': self.toy.id, 'chapter': 'Overview'}))
|
|
|
|
# Now we directly navigate to a section in a different chapter
|
|
self.check_for_get_code(200, reverse('courseware_section',
|
|
kwargs={'course_id': self.toy.id,
|
|
'chapter': 'secret:magic', 'section': 'toyvideo'}))
|
|
|
|
# And now hitting the courseware tab should redirect to 'secret:magic'
|
|
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
|
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
|
kwargs={'course_id': self.toy.id, 'chapter': 'secret:magic'}))
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE)
|
|
class TestDraftModuleStore(TestCase):
|
|
def test_get_items_with_course_items(self):
|
|
store = modulestore()
|
|
# fix was to allow get_items() to take the course_id parameter
|
|
store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0)
|
|
# test success is just getting through the above statement. The bug was that 'course_id' argument was
|
|
# not allowed to be passed in (i.e. was throwing exception)
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
|
class TestViewAuth(PageLoader):
|
|
"""Check that view authentication works properly"""
|
|
|
|
# NOTE: setUpClass() runs before override_settings takes effect, so
|
|
# can't do imports there without manually hacking settings.
|
|
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
|
|
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
|
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
|
|
|
# Create two accounts
|
|
self.student = 'view@test.com'
|
|
self.instructor = 'view2@test.com'
|
|
self.password = 'foo'
|
|
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)
|
|
|
|
def test_instructor_pages(self):
|
|
"""Make sure only instructors for the course or staff can load the instructor
|
|
dashboard, the grade views, and student profile pages"""
|
|
|
|
# First, try with an enrolled student
|
|
self.login(self.student, self.password)
|
|
# shouldn't work before enroll
|
|
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
|
self.assertRedirectsNoFollow(response, reverse('about_course', args=[self.toy.id]))
|
|
self.enroll(self.toy)
|
|
self.enroll(self.full)
|
|
# should work now -- redirect to first page
|
|
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
|
self.assertRedirectsNoFollow(response, reverse('courseware_section', kwargs={'course_id': self.toy.id,
|
|
'chapter': 'Overview',
|
|
'section': 'Toy_Videos'}))
|
|
|
|
def instructor_urls(course):
|
|
"list of urls that only instructors/staff should be able to see"
|
|
urls = [reverse(name, kwargs={'course_id': course.id}) for name in (
|
|
'instructor_dashboard',
|
|
'gradebook',
|
|
'grade_summary',)]
|
|
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
|
|
'student_id': user(self.student).id}))
|
|
return urls
|
|
|
|
# shouldn't be able to get to the instructor pages
|
|
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
|
print 'checking for 404 on {0}'.format(url)
|
|
self.check_for_get_code(404, url)
|
|
|
|
# Make the instructor staff in the toy course
|
|
group_name = _course_staff_group_name(self.toy.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(user(self.instructor))
|
|
|
|
self.logout()
|
|
self.login(self.instructor, self.password)
|
|
|
|
# Now should be able to get to the toy course, but not the full course
|
|
for url in instructor_urls(self.toy):
|
|
print 'checking for 200 on {0}'.format(url)
|
|
self.check_for_get_code(200, url)
|
|
|
|
for url in instructor_urls(self.full):
|
|
print 'checking for 404 on {0}'.format(url)
|
|
self.check_for_get_code(404, url)
|
|
|
|
|
|
# now also make the instructor staff
|
|
u = user(self.instructor)
|
|
u.is_staff = True
|
|
u.save()
|
|
|
|
# and now should be able to load both
|
|
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
|
print 'checking for 200 on {0}'.format(url)
|
|
self.check_for_get_code(200, url)
|
|
|
|
|
|
def run_wrapped(self, test):
|
|
"""
|
|
test.py turns off start dates. Enable them.
|
|
Because settings is global, be careful not to mess it up for other tests
|
|
(Can't use override_settings because we're only changing part of the
|
|
MITX_FEATURES dict)
|
|
"""
|
|
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
|
|
|
|
try:
|
|
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
|
|
test()
|
|
finally:
|
|
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
|
|
|
|
|
|
def test_dark_launch(self):
|
|
"""Make sure that before course start, students can't access course
|
|
pages, but instructors can"""
|
|
self.run_wrapped(self._do_test_dark_launch)
|
|
|
|
def test_enrollment_period(self):
|
|
"""Check that enrollment periods work"""
|
|
self.run_wrapped(self._do_test_enrollment_period)
|
|
|
|
def test_beta_period(self):
|
|
"""Check that beta-test access works"""
|
|
self.run_wrapped(self._do_test_beta_period)
|
|
|
|
def _do_test_dark_launch(self):
|
|
"""Actually do the test, relying on settings to be right."""
|
|
|
|
# Make courses start in the future
|
|
tomorrow = time.time() + 24 * 3600
|
|
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
|
self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
|
|
|
self.assertFalse(self.toy.has_started())
|
|
self.assertFalse(self.full.has_started())
|
|
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
|
|
|
def reverse_urls(names, course):
|
|
"""Reverse a list of course urls"""
|
|
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
|
|
|
|
def dark_student_urls(course):
|
|
"""
|
|
list of urls that students should be able to see only
|
|
after launch, but staff should see before
|
|
"""
|
|
urls = reverse_urls(['info', 'progress'], course)
|
|
urls.extend([
|
|
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
|
|
for book in course.textbooks
|
|
])
|
|
return urls
|
|
|
|
def light_student_urls(course):
|
|
"""
|
|
list of urls that students should be able to see before
|
|
launch.
|
|
"""
|
|
urls = reverse_urls(['about_course'], course)
|
|
urls.append(reverse('courses'))
|
|
# Need separate test for change_enrollment, since it's a POST view
|
|
#urls.append(reverse('change_enrollment'))
|
|
|
|
return urls
|
|
|
|
def instructor_urls(course):
|
|
"""list of urls that only instructors/staff should be able to see"""
|
|
urls = reverse_urls(['instructor_dashboard', 'gradebook', 'grade_summary'],
|
|
course)
|
|
return urls
|
|
|
|
def check_non_staff(course):
|
|
"""Check that access is right for non-staff in course"""
|
|
print '=== Checking non-staff access for {0}'.format(course.id)
|
|
for url in instructor_urls(course) + dark_student_urls(course) + reverse_urls(['courseware'], course):
|
|
print 'checking for 404 on {0}'.format(url)
|
|
self.check_for_get_code(404, url)
|
|
|
|
for url in light_student_urls(course):
|
|
print 'checking for 200 on {0}'.format(url)
|
|
self.check_for_get_code(200, url)
|
|
|
|
def check_staff(course):
|
|
"""Check that access is right for staff in course"""
|
|
print '=== Checking staff access for {0}'.format(course.id)
|
|
for url in (instructor_urls(course) +
|
|
dark_student_urls(course) +
|
|
light_student_urls(course)):
|
|
print 'checking for 200 on {0}'.format(url)
|
|
self.check_for_get_code(200, url)
|
|
|
|
# The student progress tab is not accessible to a student
|
|
# before launch, so the instructor view-as-student feature should return a 404 as well.
|
|
# TODO (vshnayder): If this is not the behavior we want, will need
|
|
# to make access checking smarter and understand both the effective
|
|
# user (the student), and the requesting user (the prof)
|
|
url = reverse('student_progress', kwargs={'course_id': course.id,
|
|
'student_id': user(self.student).id})
|
|
print 'checking for 404 on view-as-student: {0}'.format(url)
|
|
self.check_for_get_code(404, url)
|
|
|
|
# The courseware url should redirect, not 200
|
|
url = reverse_urls(['courseware'], course)[0]
|
|
self.check_for_get_code(302, url)
|
|
|
|
|
|
# First, try with an enrolled student
|
|
print '=== Testing student access....'
|
|
self.login(self.student, self.password)
|
|
self.enroll(self.toy)
|
|
self.enroll(self.full)
|
|
|
|
# shouldn't be able to get to anything except the light pages
|
|
check_non_staff(self.toy)
|
|
check_non_staff(self.full)
|
|
|
|
print '=== Testing course instructor access....'
|
|
# Make the instructor staff in the toy course
|
|
group_name = _course_staff_group_name(self.toy.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(user(self.instructor))
|
|
|
|
self.logout()
|
|
self.login(self.instructor, self.password)
|
|
# Enroll in the classes---can't see courseware otherwise.
|
|
self.enroll(self.toy)
|
|
self.enroll(self.full)
|
|
|
|
# should now be able to get to everything for toy course
|
|
check_non_staff(self.full)
|
|
check_staff(self.toy)
|
|
|
|
print '=== Testing staff access....'
|
|
# now also make the instructor staff
|
|
u = user(self.instructor)
|
|
u.is_staff = True
|
|
u.save()
|
|
|
|
# and now should be able to load both
|
|
check_staff(self.toy)
|
|
check_staff(self.full)
|
|
|
|
def _do_test_enrollment_period(self):
|
|
"""Actually do the test, relying on settings to be right."""
|
|
|
|
# Make courses start in the future
|
|
tomorrow = time.time() + 24 * 3600
|
|
nextday = tomorrow + 24 * 3600
|
|
yesterday = time.time() - 24 * 3600
|
|
|
|
print "changing"
|
|
# toy course's enrollment period hasn't started
|
|
self.toy.enrollment_start = time.gmtime(tomorrow)
|
|
self.toy.enrollment_end = time.gmtime(nextday)
|
|
|
|
# full course's has
|
|
self.full.enrollment_start = time.gmtime(yesterday)
|
|
self.full.enrollment_end = time.gmtime(tomorrow)
|
|
|
|
print "login"
|
|
# First, try with an enrolled student
|
|
print '=== Testing student access....'
|
|
self.login(self.student, self.password)
|
|
self.assertFalse(self.try_enroll(self.toy))
|
|
self.assertTrue(self.try_enroll(self.full))
|
|
|
|
print '=== Testing course instructor access....'
|
|
# Make the instructor staff in the toy course
|
|
group_name = _course_staff_group_name(self.toy.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(user(self.instructor))
|
|
|
|
print "logout/login"
|
|
self.logout()
|
|
self.login(self.instructor, self.password)
|
|
print "Instructor should be able to enroll in toy course"
|
|
self.assertTrue(self.try_enroll(self.toy))
|
|
|
|
print '=== Testing staff access....'
|
|
# now make the instructor global staff, but not in the instructor group
|
|
g.user_set.remove(user(self.instructor))
|
|
u = user(self.instructor)
|
|
u.is_staff = True
|
|
u.save()
|
|
|
|
# unenroll and try again
|
|
self.unenroll(self.toy)
|
|
self.assertTrue(self.try_enroll(self.toy))
|
|
|
|
def _do_test_beta_period(self):
|
|
"""Actually test beta periods, relying on settings to be right."""
|
|
|
|
# trust, but verify :)
|
|
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
|
|
|
|
# Make courses start in the future
|
|
tomorrow = time.time() + 24 * 3600
|
|
nextday = tomorrow + 24 * 3600
|
|
yesterday = time.time() - 24 * 3600
|
|
|
|
# toy course's hasn't started
|
|
self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow))
|
|
self.assertFalse(self.toy.has_started())
|
|
|
|
# but should be accessible for beta testers
|
|
self.toy.metadata['days_early_for_beta'] = '2'
|
|
|
|
# student user shouldn't see it
|
|
student_user = user(self.student)
|
|
self.assertFalse(has_access(student_user, self.toy, 'load'))
|
|
|
|
# now add the student to the beta test group
|
|
group_name = course_beta_test_group_name(self.toy.location)
|
|
g = Group.objects.create(name=group_name)
|
|
g.user_set.add(student_user)
|
|
|
|
# now the student should see it
|
|
self.assertTrue(has_access(student_user, self.toy, 'load'))
|
|
|
|
|
|
|
|
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
|
class TestCourseGrader(PageLoader):
|
|
"""Check that a course gets graded properly"""
|
|
|
|
# NOTE: setUpClass() runs before override_settings takes effect, so
|
|
# can't do imports there without manually hacking settings.
|
|
|
|
def setUp(self):
|
|
xmodule.modulestore.django._MODULESTORES = {}
|
|
courses = modulestore().get_courses()
|
|
|
|
def find_course(course_id):
|
|
"""Assumes the course is present"""
|
|
return [c for c in courses if c.id == course_id][0]
|
|
|
|
self.graded_course = find_course("edX/graded/2012_Fall")
|
|
|
|
# create a test student
|
|
self.student = 'view@test.com'
|
|
self.password = 'foo'
|
|
self.create_account('u1', self.student, self.password)
|
|
self.activate_user(self.student)
|
|
self.enroll(self.graded_course)
|
|
|
|
self.student_user = user(self.student)
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
def get_grade_summary(self):
|
|
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
|
self.graded_course.id, self.student_user, self.graded_course)
|
|
|
|
fake_request = self.factory.get(reverse('progress',
|
|
kwargs={'course_id': self.graded_course.id}))
|
|
|
|
return grades.grade(self.student_user, fake_request,
|
|
self.graded_course, student_module_cache)
|
|
|
|
def get_homework_scores(self):
|
|
return self.get_grade_summary()['totaled_scores']['Homework']
|
|
|
|
def get_progress_summary(self):
|
|
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
|
self.graded_course.id, self.student_user, self.graded_course)
|
|
|
|
fake_request = self.factory.get(reverse('progress',
|
|
kwargs={'course_id': self.graded_course.id}))
|
|
|
|
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
|
self.graded_course, student_module_cache)
|
|
return progress_summary
|
|
|
|
def check_grade_percent(self, percent):
|
|
grade_summary = self.get_grade_summary()
|
|
self.assertEqual(grade_summary['percent'], percent)
|
|
|
|
def submit_question_answer(self, problem_url_name, responses):
|
|
"""
|
|
The field names of a problem are hard to determine. This method only works
|
|
for the problems used in the edX/graded course, which has fields named in the
|
|
following form:
|
|
input_i4x-edX-graded-problem-H1P3_2_1
|
|
input_i4x-edX-graded-problem-H1P3_2_2
|
|
"""
|
|
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
|
|
|
modx_url = reverse('modx_dispatch',
|
|
kwargs={
|
|
'course_id': self.graded_course.id,
|
|
'location': problem_location,
|
|
'dispatch': 'problem_check', }
|
|
)
|
|
|
|
resp = self.client.post(modx_url, {
|
|
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
|
|
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
|
|
})
|
|
print "modx_url", modx_url, "responses", responses
|
|
print "resp", resp
|
|
|
|
return resp
|
|
|
|
def problem_location(self, problem_url_name):
|
|
return "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
|
|
|
def reset_question_answer(self, problem_url_name):
|
|
problem_location = self.problem_location(problem_url_name)
|
|
|
|
modx_url = reverse('modx_dispatch',
|
|
kwargs={
|
|
'course_id': self.graded_course.id,
|
|
'location': problem_location,
|
|
'dispatch': 'problem_reset', }
|
|
)
|
|
|
|
resp = self.client.post(modx_url)
|
|
return resp
|
|
|
|
def test_get_graded(self):
|
|
#### Check that the grader shows we have 0% in the course
|
|
self.check_grade_percent(0)
|
|
|
|
#### Submit the answers to a few problems as ajax calls
|
|
def earned_hw_scores():
|
|
"""Global scores, each Score is a Problem Set"""
|
|
return [s.earned for s in self.get_homework_scores()]
|
|
|
|
def score_for_hw(hw_url_name):
|
|
hw_section = [section for section
|
|
in self.get_progress_summary()[0]['sections']
|
|
if section.get('url_name') == hw_url_name][0]
|
|
return [s.earned for s in hw_section['scores']]
|
|
|
|
# Only get half of the first problem correct
|
|
self.submit_question_answer('H1P1', ['Correct', 'Incorrect'])
|
|
self.check_grade_percent(0.06)
|
|
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
|
|
self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0])
|
|
|
|
# Get both parts of the first problem correct
|
|
self.reset_question_answer('H1P1')
|
|
self.submit_question_answer('H1P1', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.13)
|
|
self.assertEqual(earned_hw_scores(), [2.0, 0, 0])
|
|
self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0])
|
|
|
|
# This problem is shown in an ABTest
|
|
self.submit_question_answer('H1P2', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.25)
|
|
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
|
|
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
|
|
|
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
|
|
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.25)
|
|
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
|
|
|
# On the second homework, we only answer half of the questions.
|
|
# Then it will be dropped when homework three becomes the higher percent
|
|
# This problem is also weighted to be 4 points (instead of default of 2)
|
|
# If the problem was unweighted the percent would have been 0.38 so we
|
|
# know it works.
|
|
self.submit_question_answer('H2P1', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.42)
|
|
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
|
|
|
|
# Third homework
|
|
self.submit_question_answer('H3P1', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.42) # Score didn't change
|
|
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
|
|
|
|
self.submit_question_answer('H3P2', ['Correct', 'Correct'])
|
|
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
|
|
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
|
|
|
|
# Now we answer the final question (worth half of the grade)
|
|
self.submit_question_answer('FinalQuestion', ['Correct', 'Correct'])
|
|
self.check_grade_percent(1.0) # Hooray! We got 100%
|