Commit 7d0b72a4 by jnater

Merge pull request #204 from edx/jnater/courseware_tests

Jnater/courseware tests
parents be4d5f32 af05d05c
......@@ -52,8 +52,8 @@ class Location(_LocationBase):
Locations representations of URLs of the
form {tag}://{org}/{course}/{category}/{name}[@{revision}]
However, they can also be represented a dictionaries (specifying each component),
tuples or list (specified in order), or as strings of the url
However, they can also be represented as dictionaries (specifying each component),
tuples or lists (specified in order), or as strings of the url
'''
__slots__ = ()
......
......@@ -15,10 +15,26 @@ class ModuleStoreTestCase(TestCase):
and drops it they are finished. """
@staticmethod
def update_course(course, data):
"""
Updates the version of course in the modulestore
with the metadata in 'data' and returns the updated version.
'course' is an instance of CourseDescriptor for which we want
to update metadata.
'data' is a dictionary with an entry for each CourseField we want to update.
"""
store = xmodule.modulestore.django.modulestore()
store.update_metadata(course.location, data)
updated_course = store.get_instance(course.id, course.location)
return updated_course
@staticmethod
def flush_mongo_except_templates():
'''
Delete everything in the module store except templates
'''
"""
Delete everything in the module store except templates.
"""
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
......@@ -31,11 +47,11 @@ class ModuleStoreTestCase(TestCase):
@staticmethod
def load_templates_if_necessary():
'''
"""
Load templates into the direct modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
XModules such as sections and problems.
"""
modulestore = xmodule.modulestore.django.modulestore('direct')
# Count the number of templates
......@@ -47,9 +63,9 @@ class ModuleStoreTestCase(TestCase):
@classmethod
def setUpClass(cls):
'''
Flush the mongo store and set up templates
'''
"""
Flush the mongo store and set up templates.
"""
# Use a uuid to differentiate
# the mongo collections on jenkins.
......@@ -67,9 +83,9 @@ class ModuleStoreTestCase(TestCase):
@classmethod
def tearDownClass(cls):
'''
Revert to the old modulestore settings
'''
"""
Revert to the old modulestore settings.
"""
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
......@@ -81,9 +97,9 @@ class ModuleStoreTestCase(TestCase):
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self):
'''
Remove everything but the templates before each test
'''
"""
Remove everything but the templates before each test.
"""
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
......@@ -95,9 +111,9 @@ class ModuleStoreTestCase(TestCase):
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
'''
Flush everything we created except the templates
'''
"""
Flush everything we created except the templates.
"""
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
......
from factory import Factory, lazy_attribute_sequence, lazy_attribute
from uuid import uuid4
import datetime
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
from xblock.runtime import InvalidScopeError
import datetime
from pytz import UTC
......@@ -59,6 +60,10 @@ class XModuleCourseFactory(Factory):
if data is not None:
store.update_item(new_course.location, data)
# update_item updates the the course as it exists in the modulestore, but doesn't
# update the instance we are working with, so have to refetch the course after updating it.
new_course = store.get_instance(new_course.id, new_course.location)
return new_course
......@@ -147,6 +152,10 @@ class XModuleItemFactory(Factory):
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()])
# update_children updates the the item as it exists in the modulestore, but doesn't
# update the instance we are working with, so have to refetch the item after updating it.
new_item = store.get_item(new_item.location)
return new_item
......@@ -181,6 +190,7 @@ def get_test_xmodule_for_descriptor(descriptor):
)
return descriptor.xmodule(module_sys)
def _test_xblock_model_data_accessor(descriptor):
simple_map = {}
for field in descriptor.fields:
......
......@@ -586,7 +586,6 @@ def _has_access_to_location(user, location, access_level, course_context):
debug("Deny: user not in groups %s", instructor_groups)
else:
log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level)
return False
......
import json
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from student.models import Registration
from django.test import TestCase
def check_for_get_code(self, code, url):
"""
Check that we got the expected code when accessing url via GET.
Returns the HTTP response.
`self` is a class that subclasses TestCase.
`code` is a status code for HTTP responses.
`url` is a url pattern for which we have to test the response.
"""
resp = self.client.get(url)
self.assertEqual(resp.status_code, code,
"got code %d for url '%s'. Expected code %d"
% (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 HTTP response.
`self` is a class that subclasses TestCase.
`code` is a status code for HTTP responses.
`url` is a url pattern for which we want to test the response.
"""
resp = self.client.post(url, data)
self.assertEqual(resp.status_code, code,
"got code %d for url '%s'. Expected code %d"
% (resp.status_code, url, code))
return resp
class LoginEnrollmentTestCase(TestCase):
"""
Provides support for user creation,
activation, login, and course enrollment.
"""
def setup_user(self):
"""
Create a user account, activate, and log in.
"""
self.email = 'foo@test.com'
self.password = 'bar'
self.username = 'test'
self.create_account(self.username,
self.email, self.password)
self.activate_user(self.email)
self.login(self.email, self.password)
# ============ User creation and login ==============
def login(self, email, password):
"""
Login, check that the corresponding view's response has a 200 status code.
"""
resp = self.client.post(reverse('login'),
{'email': email, 'password': password})
self.assertEqual(resp.status_code, 200)
data = json.loads(resp.content)
self.assertTrue(data['success'])
def logout(self):
"""
Logout; check that the HTTP response code indicates redirection
as expected.
"""
# should redirect
check_for_get_code(self, 302, reverse('logout'))
def create_account(self, username, email, password):
"""
Create the account and check that it worked.
"""
resp = check_for_post_code(self, 200, reverse('create_account'), {
'username': username,
'email': email,
'password': password,
'name': 'username',
'terms_of_service': 'true',
'honor_code': 'true',
})
data = json.loads(resp.content)
self.assertEqual(data['success'], True)
# Check both that the user is created, and inactive
self.assertFalse(User.objects.get(email=email).is_active)
def activate_user(self, email):
"""
Look up the activation key for the user, then hit the activate view.
No error checking.
"""
activation_key = Registration.objects.get(user__email=email).activation_key
# and now we try to activate
check_for_get_code(self, 200, reverse('activate', kwargs={'key': activation_key}))
# Now make sure that the user is now actually activated
self.assertTrue(User.objects.get(email=email).is_active)
def enroll(self, course, verify=False):
"""
Try to enroll and return boolean indicating result.
`course` is an instance of CourseDescriptor.
`verify` is an optional boolean parameter specifying whether we
want to verify that the student was successfully enrolled
in the course.
"""
resp = self.client.post(reverse('change_enrollment'), {
'enrollment_action': 'enroll',
'course_id': course.id,
})
result = resp.status_code == 200
if verify:
self.assertTrue(result)
return result
def unenroll(self, course):
"""
Unenroll the currently logged-in user, and check that it worked.
`course` is an instance of CourseDescriptor.
"""
check_for_post_code(self, 200, reverse('change_enrollment'), {'enrollment_action': 'unenroll',
'course_id': course.id})
from uuid import uuid4
from django.conf import settings
def mongo_store_config(data_dir):
"""
Defines default module store using MongoModuleStore.
Use of this config requires mongo to be running.
"""
store = {
'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'
}
}
}
store['direct'] = store['default']
return store
def draft_mongo_store_config(data_dir):
"""
Defines default module store using DraftMongoModuleStore.
"""
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string'
}
return {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
}
}
def xml_store_config(data_dir):
"""
Defines default module store using XMLModuleStore.
"""
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)
from django.test import TestCase
from django.test.utils import override_settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from modulestore_config import TEST_DATA_DRAFT_MONGO_MODULESTORE
@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)
......@@ -12,13 +12,15 @@ from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
import json
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
'''
......@@ -41,7 +43,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
g.user_set.add(User.objects.get(email=self.instructor))
make_instructor(self.graded_course)
......@@ -67,7 +69,6 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase):
self.assertTrue(sdebug in resp.content)
def toggle_masquerade(self):
'''
Toggle masquerade state
......
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from helpers import LoginEnrollmentTestCase, check_for_get_code
from modulestore_config import TEST_DATA_MONGO_MODULESTORE
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that navigation state is saved properly.
"""
STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
def setUp(self):
self.test_course = CourseFactory.create(display_name='Robot_Sub_Course')
self.course = CourseFactory.create(display_name='Robot_Super_Course')
self.chapter0 = ItemFactory.create(parent_location=self.course.location,
display_name='Overview')
self.chapter9 = ItemFactory.create(parent_location=self.course.location,
display_name='factory_chapter')
self.section0 = ItemFactory.create(parent_location=self.chapter0.location,
display_name='Welcome')
self.section9 = ItemFactory.create(parent_location=self.chapter9.location,
display_name='factory_section')
# Create student accounts and activate them.
for i in range(len(self.STUDENT_INFO)):
email, password = self.STUDENT_INFO[i]
username = 'u{0}'.format(i)
self.create_account(username, email, password)
self.activate_user(email)
def test_redirects_first_time(self):
"""
Verify that the first time we click on the courseware tab we are
redirected to the 'Welcome' section.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
self.assertRedirects(resp, reverse(
'courseware_section', kwargs={'course_id': self.course.id,
'chapter': 'Overview',
'section': 'Welcome'}))
def test_redirects_second_time(self):
"""
Verify the accordion remembers we've already visited the Welcome section
and redirects correpondingly.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
self.client.get(reverse('courseware_section', kwargs={'course_id': self.course.id,
'chapter': 'Overview',
'section': 'Welcome'}))
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
self.assertRedirects(resp, reverse('courseware_chapter',
kwargs={'course_id': self.course.id,
'chapter': 'Overview'}))
def test_accordion_state(self):
"""
Verify the accordion remembers which chapter you were last viewing.
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
# Now we directly navigate to a section in a chapter other than 'Overview'.
check_for_get_code(self, 200, reverse('courseware_section',
kwargs={'course_id': self.course.id,
'chapter': 'factory_chapter',
'section': 'factory_section'}))
# And now hitting the courseware tab should redirect to 'factory_chapter'
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
self.assertRedirects(resp, reverse('courseware_chapter',
kwargs={'course_id': self.course.id,
'chapter': 'factory_chapter'}))
import datetime
import pytz
from mock import patch
from django.contrib.auth.models import User, Group
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from courseware.access import (has_access, _course_staff_group_name,
course_beta_test_group_name, settings as access_settings)
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from helpers import LoginEnrollmentTestCase, check_for_get_code
from modulestore_config import TEST_DATA_MONGO_MODULESTORE
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Check that view authentication works properly.
"""
ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
@staticmethod
def _reverse_urls(names, course):
"""
Reverse a list of course urls.
`names` is a list of URL names that correspond to sections in a course.
`course` is the instance of CourseDescriptor whose section URLs are to be returned.
Returns a list URLs corresponding to section in the passed in course.
"""
return [reverse(name, kwargs={'course_id': course.id})
for name in names]
def _check_non_staff_light(self, course):
"""
Check that non-staff have access to light urls.
`course` is an instance of CourseDescriptor.
"""
urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')]
for url in urls:
check_for_get_code(self, 200, url)
def _check_non_staff_dark(self, course):
"""
Check that non-staff don't have access to dark urls.
"""
names = ['courseware', 'instructor_dashboard', 'progress']
urls = self._reverse_urls(names, course)
urls.extend([
reverse('book', kwargs={'course_id': course.id,
'book_index': index})
for index, book in enumerate(course.textbooks)
])
for url in urls:
check_for_get_code(self, 404, url)
def _check_staff(self, course):
"""
Check that access is right for staff in course.
"""
names = ['about_course', 'instructor_dashboard', 'progress']
urls = self._reverse_urls(names, course)
urls.extend([
reverse('book', kwargs={'course_id': course.id,
'book_index': index})
for index, book in enumerate(course.textbooks)
])
for url in urls:
check_for_get_code(self, 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.objects.get(email=self.ACCOUNT_INFO[0][0]).id})
check_for_get_code(self, 404, url)
# The courseware url should redirect, not 200
url = self._reverse_urls(['courseware'], course)[0]
check_for_get_code(self, 302, url)
def setUp(self):
self.course = CourseFactory.create(number='999', display_name='Robot_Super_Course')
self.overview_chapter = ItemFactory.create(display_name='Overview')
self.courseware_chapter = ItemFactory.create(display_name='courseware')
self.test_course = CourseFactory.create(number='666', display_name='Robot_Sub_Course')
self.sub_courseware_chapter = ItemFactory.create(parent_location=self.test_course.location,
display_name='courseware')
self.sub_overview_chapter = ItemFactory.create(parent_location=self.sub_courseware_chapter.location,
display_name='Overview')
self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location,
display_name='Welcome')
# Create two accounts and activate them.
for i in range(len(self.ACCOUNT_INFO)):
username, email, password = 'u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1]
self.create_account(username, email, password)
self.activate_user(email)
def test_redirection_unenrolled(self):
"""
Verify unenrolled student is redirected to the 'about' section of the chapter
instead of the 'Welcome' section after clicking on the courseware tab.
"""
email, password = self.ACCOUNT_INFO[0]
self.login(email, password)
response = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
self.assertRedirects(response,
reverse('about_course',
args=[self.course.id]))
def test_redirection_enrolled(self):
"""
Verify enrolled student is redirected to the 'Welcome' section of
the chapter after clicking on the courseware tab.
"""
email, password = self.ACCOUNT_INFO[0]
self.login(email, password)
self.enroll(self.course)
response = self.client.get(reverse('courseware',
kwargs={'course_id': self.course.id}))
self.assertRedirects(response,
reverse('courseware_section',
kwargs={'course_id': self.course.id,
'chapter': 'Overview',
'section': 'Welcome'}))
def test_instructor_page_access_nonstaff(self):
"""
Verify non-staff cannot load the instructor
dashboard, the grade views, and student profile pages.
"""
email, password = self.ACCOUNT_INFO[0]
self.login(email, password)
self.enroll(self.course)
self.enroll(self.test_course)
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})]
# Shouldn't be able to get to the instructor pages
for url in urls:
check_for_get_code(self, 404, url)
def test_instructor_course_access(self):
"""
Verify instructor can load the instructor dashboard, the grade views,
and student profile pages for their course.
"""
email, password = self.ACCOUNT_INFO[1]
# Make the instructor staff in self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=email))
self.login(email, password)
# Now should be able to get to self.course, but not self.test_course
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
check_for_get_code(self, 200, url)
url = reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})
check_for_get_code(self, 404, url)
def test_instructor_as_staff_access(self):
"""
Verify the instructor can load staff pages if he is given
staff permissions.
"""
email, password = self.ACCOUNT_INFO[1]
self.login(email, password)
# now make the instructor also staff
instructor = User.objects.get(email=email)
instructor.is_staff = True
instructor.save()
# and now should be able to load both
urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}),
reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})]
for url in urls:
check_for_get_code(self, 200, url)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
def test_dark_launch_enrolled_student(self):
"""
Make sure that before course start, students can't access course
pages.
"""
student_email, student_password = self.ACCOUNT_INFO[0]
# Make courses start in the future
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
course_data = {'start': tomorrow}
test_course_data = {'start': tomorrow}
self.course = self.update_course(self.course, course_data)
self.test_course = self.update_course(self.test_course, test_course_data)
self.assertFalse(self.course.has_started())
self.assertFalse(self.test_course.has_started())
# First, try with an enrolled student
self.login(student_email, student_password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
# shouldn't be able to get to anything except the light pages
self._check_non_staff_light(self.course)
self._check_non_staff_dark(self.course)
self._check_non_staff_light(self.test_course)
self._check_non_staff_dark(self.test_course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
def test_dark_launch_instructor(self):
"""
Make sure that before course start instructors can access the
page for their course.
"""
instructor_email, instructor_password = self.ACCOUNT_INFO[1]
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
course_data = {'start': tomorrow}
test_course_data = {'start': tomorrow}
self.course = self.update_course(self.course, course_data)
self.test_course = self.update_course(self.test_course, test_course_data)
# Make the instructor staff in self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=instructor_email))
self.logout()
self.login(instructor_email, instructor_password)
# Enroll in the classes---can't see courseware otherwise.
self.enroll(self.course, True)
self.enroll(self.test_course, True)
# should now be able to get to everything for self.course
self._check_non_staff_light(self.test_course)
self._check_non_staff_dark(self.test_course)
self._check_staff(self.course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
def test_dark_launch_staff(self):
"""
Make sure that before course start staff can access
course pages.
"""
instructor_email, instructor_password = self.ACCOUNT_INFO[1]
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
course_data = {'start': tomorrow}
test_course_data = {'start': tomorrow}
self.course = self.update_course(self.course, course_data)
self.test_course = self.update_course(self.test_course, test_course_data)
self.login(instructor_email, instructor_password)
self.enroll(self.course, True)
self.enroll(self.test_course, True)
# now also make the instructor staff
instructor = User.objects.get(email=instructor_email)
instructor.is_staff = True
instructor.save()
# and now should be able to load both
self._check_staff(self.course)
self._check_staff(self.test_course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
def test_enrollment_period(self):
"""
Check that enrollment periods work.
"""
student_email, student_password = self.ACCOUNT_INFO[0]
instructor_email, instructor_password = self.ACCOUNT_INFO[1]
# Make courses start in the future
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
nextday = tomorrow + datetime.timedelta(days=1)
yesterday = now - datetime.timedelta(days=1)
course_data = {'enrollment_start': tomorrow, 'enrollment_end': nextday}
test_course_data = {'enrollment_start': yesterday, 'enrollment_end': tomorrow}
# self.course's enrollment period hasn't started
self.course = self.update_course(self.course, course_data)
# test_course course's has
self.test_course = self.update_course(self.test_course, test_course_data)
# First, try with an enrolled student
self.login(student_email, student_password)
self.assertFalse(self.enroll(self.course))
self.assertTrue(self.enroll(self.test_course))
# Make the instructor staff in the self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=instructor_email))
self.logout()
self.login(instructor_email, instructor_password)
self.assertTrue(self.enroll(self.course))
# now make the instructor global staff, but not in the instructor group
group.user_set.remove(User.objects.get(email=instructor_email))
instructor = User.objects.get(email=instructor_email)
instructor.is_staff = True
instructor.save()
# unenroll and try again
self.unenroll(self.course)
self.assertTrue(self.enroll(self.course))
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
def test_beta_period(self):
"""
Check that beta-test access works.
"""
student_email, student_password = self.ACCOUNT_INFO[0]
instructor_email, instructor_password = self.ACCOUNT_INFO[1]
# Make courses start in the future
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
course_data = {'start': tomorrow}
# self.course's hasn't started
self.course = self.update_course(self.course, course_data)
self.assertFalse(self.course.has_started())
# but should be accessible for beta testers
self.course.lms.days_early_for_beta = 2
# student user shouldn't see it
student_user = User.objects.get(email=student_email)
self.assertFalse(has_access(student_user, self.course, 'load'))
# now add the student to the beta test group
group_name = course_beta_test_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(student_user)
# now the student should see it
self.assertTrue(has_access(student_user, self.course, 'load'))
'''
Test for lms courseware app
'''
import logging
import json
import random
from urlparse import urlsplit, urlunsplit
from uuid import uuid4
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
# Need access to internal func to put users in the right group
from courseware import grades
from courseware.model_data import ModelDataCache
from courseware.access import (has_access, _course_staff_group_name,
course_beta_test_group_name)
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
import datetime
from django.utils.timezone import UTC
log = logging.getLogger("mitx." + __name__)
def parse_json(response):
"""Parse response, which is assumed to be json"""
return json.loads(response.content)
def get_user(email):
'''look up a user by email'''
return User.objects.get(email=email)
def get_registration(email):
'''look up registration object by email'''
return Registration.objects.get(user__email=email)
def mongo_store_config(data_dir):
'''
Defines default module store using MongoModuleStore
Use of this config requires mongo to be running
'''
store = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
store['direct'] = store['default']
return store
def draft_mongo_store_config(data_dir):
'''Defines default module store using DraftMongoModuleStore'''
return {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore_%s' % uuid4().hex,
'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
def xml_store_config(data_dir):
'''Defines default module store using XMLModuleStore'''
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 LoginEnrollmentTestCase(TestCase):
'''
Base TestCase providing support for user creation,
activation, login, and course enrollment
'''
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 %d instead of 302'
% (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 '%s', expected '%s'" %
(url, expected_url))
def setup_viewtest_user(self):
'''create a user account, activate, and log in'''
self.viewtest_email = 'view@test.com'
self.viewtest_password = 'foo'
self.viewtest_username = 'viewtest'
self.create_account(self.viewtest_username,
self.viewtest_email, self.viewtest_password)
self.activate_user(self.viewtest_email)
self.login(self.viewtest_email, self.viewtest_password)
from helpers import LoginEnrollmentTestCase
from modulestore_config import TEST_DATA_DIR, TEST_DATA_XML_MODULESTORE, TEST_DATA_MONGO_MODULESTORE, TEST_DATA_DRAFT_MONGO_MODULESTORE
# ============ User creation and login ==============
def _login(self, email, password):
'''Login. View should always return 200. The success/fail is in the
returned json'''
resp = self.client.post(reverse('login'),
{'email': email, 'password': password})
self.assertEqual(resp.status_code, 200)
return resp
def login(self, email, password):
'''Login, check that it worked.'''
resp = self._login(email, password)
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, password):
'''Try to create an account. No error checking'''
resp = self.client.post('/create_account', {
'username': username,
'email': email,
'password': password,
'name': 'Fred Weasley',
'terms_of_service': 'true',
'honor_code': 'true',
})
return resp
def create_account(self, username, email, password):
'''Create the account and check that it worked'''
resp = self._create_account(username, email, password)
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(get_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 = get_registration(email).activation_key
# and now we try to activate
url = reverse('activate', kwargs={'key': activation_key})
resp = self.client.get(url)
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(get_user(email).is_active)
def try_enroll(self, course):
"""Try to enroll. Return bool success instead of asserting it."""
resp = self.client.post('/change_enrollment', {
'enrollment_action': 'enroll',
'course_id': course.id,
})
print ('Enrollment in %s result status code: %s'
% (course.location.url(), str(resp.status_code)))
return resp.status_code == 200
def enroll(self, course):
"""Enroll the currently logged-in user, and check that it worked."""
result = self.try_enroll(course)
self.assertTrue(result)
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,
})
self.assertTrue(resp.status_code == 200)
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 %d for url '%s'. Expected code %d"
% (resp.status_code, url, code))
return resp
def check_for_post_code(self, code, url, data={}):
class ActivateLoginTest(LoginEnrollmentTestCase):
"""
Check that we got the expected code when accessing url via POST.
Returns the response.
Test logging in and logging out.
"""
resp = self.client.post(url, data)
self.assertEqual(resp.status_code, code,
"got code %d for url '%s'. Expected code %d"
% (resp.status_code, url, code))
return resp
class ActivateLoginTest(LoginEnrollmentTestCase):
'''Test logging in and logging out'''
def setUp(self):
self.setup_viewtest_user()
self.setup_user()
def test_activate_login(self):
'''Test login -- the setup function does all the work'''
"""
Test login -- the setup function does all the work.
"""
pass
def test_logout(self):
'''Test logout -- setup function does login'''
"""
Test logout -- setup function does login.
"""
self.logout()
class PageLoaderTestCase(LoginEnrollmentTestCase):
''' Base class that adds a function to load all pages in a modulestore '''
"""
Base class that adds a function to load all pages in a modulestore.
"""
def check_random_page_loads(self, module_store):
'''
Choose a page in the course randomly, and assert that it loads
'''
"""
Choose a page in the course randomly, and assert that it loads.
"""
# 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)
self.enroll(course, True)
course_id = course.id
# Search for items in the course
......@@ -339,12 +99,12 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
def _assert_loads(self, django_url, kwargs, descriptor,
expect_redirect=False,
check_content=False):
'''
"""
Assert that the url loads correctly.
If expect_redirect, then also check that we were redirected.
If check_content, then check that we don't get
an error message about unavailable modules.
'''
"""
url = reverse(django_url, kwargs=kwargs)
response = self.client.get(url, follow=True)
......@@ -364,11 +124,13 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase):
'''Check that all pages in test courses load properly from XML'''
"""
Check that all pages in test courses load properly from XML.
"""
def setUp(self):
super(TestCoursesLoadTestCase_XmlModulestore, self).setUp()
self.setup_viewtest_user()
self.setup_user()
xmodule.modulestore.django._MODULESTORES = {}
def test_toy_course_loads(self):
......@@ -383,11 +145,13 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase):
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
'''Check that all pages in test courses load properly from Mongo'''
"""
Check that all pages in test courses load properly from Mongo.
"""
def setUp(self):
super(TestCoursesLoadTestCase_MongoModulestore, self).setUp()
self.setup_viewtest_user()
self.setup_user()
xmodule.modulestore.django._MODULESTORES = {}
modulestore().collection.drop()
......@@ -405,67 +169,6 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
self.assertGreater(len(course.textbooks), 0)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestNavigation(LoginEnrollmentTestCase):
"""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):
......@@ -478,558 +181,3 @@ class TestDraftModuleStore(TestCase):
# 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(LoginEnrollmentTestCase):
"""Check that view authentication works properly"""
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': get_user(self.student).id}))
return urls
# Randomly sample an instructor page
url = random.choice(instructor_urls(self.toy) +
instructor_urls(self.full))
# Shouldn't be able to get to the instructor pages
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)
group = Group.objects.create(name=group_name)
group.user_set.add(get_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
url = random.choice(instructor_urls(self.toy))
print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
url = random.choice(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
instructor = get_user(self.instructor)
instructor.is_staff = True
instructor.save()
# and now should be able to load both
url = random.choice(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 = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
self.toy.lms.start = tomorrow
self.full.lms.start = 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': index})
for index, book in enumerate(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'))
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)
# Randomly sample a dark url
url = random.choice(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)
# Randomly sample a light url
url = random.choice(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)
# Randomly sample a url
url = random.choice(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': get_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)
group = Group.objects.create(name=group_name)
group.user_set.add(get_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
instructor = get_user(self.instructor)
instructor.is_staff = True
instructor.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 = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
nextday = tomorrow + datetime.timedelta(days=1)
yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1)
print "changing"
# toy course's enrollment period hasn't started
self.toy.enrollment_start = tomorrow
self.toy.enrollment_end = nextday
# full course's has
self.full.enrollment_start = yesterday
self.full.enrollment_end = 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)
group = Group.objects.create(name=group_name)
group.user_set.add(get_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
group.user_set.remove(get_user(self.instructor))
instructor = get_user(self.instructor)
instructor.is_staff = True
instructor.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 = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
# toy course's hasn't started
self.toy.lms.start = tomorrow
self.assertFalse(self.toy.has_started())
# but should be accessible for beta testers
self.toy.lms.days_early_for_beta = 2
# student user shouldn't see it
student_user = get_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)
group = Group.objects.create(name=group_name)
group.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 TestSubmittingProblems(LoginEnrollmentTestCase):
"""Check that a course gets graded properly"""
# Subclasses should specify the course slug
course_slug = "UNKNOWN"
course_when = "UNKNOWN"
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
course_name = "edX/%s/%s" % (self.course_slug, self.course_when)
self.course = modulestore().get_course(course_name)
assert self.course, "Couldn't load course %r" % course_name
# 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.course)
self.student_user = get_user(self.student)
self.factory = RequestFactory()
def problem_location(self, problem_url_name):
return "i4x://edX/{}/problem/{}".format(self.course_slug, problem_url_name)
def modx_url(self, problem_location, dispatch):
return reverse(
'modx_dispatch',
kwargs={
'course_id': self.course.id,
'location': problem_location,
'dispatch': dispatch,
}
)
def submit_question_answer(self, problem_url_name, responses):
"""
Submit answers to a question.
Responses is a dict mapping problem ids (not sure of the right term)
to answers:
{'2_1': 'Correct', '2_2': 'Incorrect'}
"""
problem_location = self.problem_location(problem_url_name)
modx_url = self.modx_url(problem_location, 'problem_check')
answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name)
resp = self.client.post(modx_url,
{ (answer_key_prefix + k): v for k, v in responses.items() }
)
return resp
def reset_question_answer(self, problem_url_name):
'''resets specified problem for current user'''
problem_location = self.problem_location(problem_url_name)
modx_url = self.modx_url(problem_location, 'problem_reset')
resp = self.client.post(modx_url)
return resp
class TestCourseGrader(TestSubmittingProblems):
"""Check that a course gets graded properly"""
course_slug = "graded"
course_when = "2012_Fall"
def get_grade_summary(self):
'''calls grades.grade for current user and course'''
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
self.course.id, self.student_user, self.course)
fake_request = self.factory.get(reverse('progress',
kwargs={'course_id': self.course.id}))
return grades.grade(self.student_user, fake_request,
self.course, model_data_cache)
def get_homework_scores(self):
'''get scores for homeworks'''
return self.get_grade_summary()['totaled_scores']['Homework']
def get_progress_summary(self):
'''return progress summary structure for current user and course'''
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(
self.course.id, self.student_user, self.course)
fake_request = self.factory.get(reverse('progress',
kwargs={'course_id': self.course.id}))
progress_summary = grades.progress_summary(self.student_user,
fake_request,
self.course,
model_data_cache)
return progress_summary
def check_grade_percent(self, percent):
'''assert that percent grade is as expected'''
grade_summary = self.get_grade_summary()
self.assertEqual(grade_summary['percent'], percent)
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):
"""returns list of scores for a given url"""
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', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': 'Correct'})
self.check_grade_percent(0.42)
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
# Third homework
self.submit_question_answer('H3P1', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': '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', {'2_1': 'Correct', '2_2': 'Correct'})
self.check_grade_percent(1.0) # Hooray! We got 100%
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestSchematicResponse(TestSubmittingProblems):
"""Check that we can submit a schematic response, and it answers properly."""
course_slug = "embedded_python"
course_when = "2013_Spring"
def test_schematic(self):
resp = self.submit_question_answer('schematic_problem',
{ '2_1': json.dumps(
[['transient', {'Z': [
[0.0000004, 2.8],
[0.0000009, 2.8],
[0.0000014, 2.8],
[0.0000019, 2.8],
[0.0000024, 2.8],
[0.0000029, 0.2],
[0.0000034, 0.2],
[0.0000039, 0.2]
]}]]
)
})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'correct')
self.reset_question_answer('schematic_problem')
resp = self.submit_question_answer('schematic_problem',
{ '2_1': json.dumps(
[['transient', {'Z': [
[0.0000004, 2.8],
[0.0000009, 0.0], # wrong.
[0.0000014, 2.8],
[0.0000019, 2.8],
[0.0000024, 2.8],
[0.0000029, 0.2],
[0.0000034, 0.2],
[0.0000039, 0.2]
]}]]
)
})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'incorrect')
def test_check_function(self):
resp = self.submit_question_answer('cfn_problem', {'2_1': "0, 1, 2, 3, 4, 5, 'Outside of loop', 6"})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'correct')
self.reset_question_answer('cfn_problem')
resp = self.submit_question_answer('cfn_problem', {'2_1': "xyzzy!"})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'incorrect')
def test_computed_answer(self):
resp = self.submit_question_answer('computed_answer', {'2_1': "Xyzzy"})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'correct')
self.reset_question_answer('computed_answer')
resp = self.submit_question_answer('computed_answer', {'2_1': "NO!"})
respdata = json.loads(resp.content)
self.assertEqual(respdata['success'], 'incorrect')
......@@ -11,12 +11,13 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/inst
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
......@@ -45,7 +46,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
g.user_set.add(User.objects.get(email=self.instructor))
make_instructor(self.toy)
......@@ -72,7 +73,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase):
# All the not-actually-in-the-course hw and labs come from the
# default grading policy string in graders.py
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
"2","u2","username","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
'''
self.assertEqual(body, expected_body, msg)
......@@ -7,7 +7,8 @@ from django.test.utils import override_settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
......
......@@ -6,7 +6,7 @@ Unit tests for instructor dashboard forum administration
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \
......@@ -14,7 +14,8 @@ from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \
from django_comment_client.utils import has_forum_access
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
......@@ -55,7 +56,7 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(get_user(self.instructor))
g.user_set.add(User.objects.get(email=self.instructor))
self.logout()
self.login(self.instructor, self.password)
......
......@@ -8,7 +8,7 @@ import json
from mock import MagicMock, patch, Mock
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, User
from django.conf import settings
from mitxmako.shortcuts import render_to_string
......@@ -20,7 +20,6 @@ from xmodule.x_module import ModuleSystem
from open_ended_grading import staff_grading_service, views
from courseware.access import _course_staff_group_name
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user
import logging
......@@ -30,6 +29,9 @@ from django.test.utils import override_settings
from xmodule.tests import test_util_open_ended
from courseware.tests import factories
from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE
from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code, check_for_post_code
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestStaffGradingService(LoginEnrollmentTestCase):
......@@ -57,7 +59,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(get_user(self.instructor))
group.user_set.add(User.objects.get(email=self.instructor))
make_instructor(self.toy)
......@@ -74,8 +76,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
# 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)
check_for_get_code(self, 404, url)
check_for_post_code(self, 404, url)
def test_get_next(self):
self.login(self.instructor, self.password)
......@@ -83,7 +85,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
data = {'location': self.location}
response = self.check_for_post_code(200, url, data)
response = check_for_post_code(self, 200, url, data)
content = json.loads(response.content)
......@@ -112,7 +114,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
if skip:
data.update({'skipped': True})
response = self.check_for_post_code(200, url, data)
response = check_for_post_code(self, 200, url, data)
content = json.loads(response.content)
self.assertTrue(content['success'], str(content))
self.assertEquals(content['submission_id'], self.mock_service.cnt)
......@@ -129,7 +131,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id})
data = {}
response = self.check_for_post_code(200, url, data)
response = check_for_post_code(self, 200, url, data)
content = json.loads(response.content)
self.assertTrue(content['success'], str(content))
......
......@@ -37,7 +37,7 @@ urlpatterns = ('', # nopep8
url(r'^login_ajax$', 'student.views.login_user', name="login"),
url(r'^login_ajax/(?P<error>[^/]*)$', 'student.views.login_user'),
url(r'^logout$', 'student.views.logout_user', name='logout'),
url(r'^create_account$', 'student.views.create_account'),
url(r'^create_account$', 'student.views.create_account', name='create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
url(r'^begin_exam_registration/(?P<course_id>[^/]+/[^/]+/[^/]+)$', 'student.views.begin_exam_registration', name="begin_exam_registration"),
......
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