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'}))
......@@ -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)
......@@ -146,4 +147,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase):
added_roles.append(rolename)
added_roles.sort()
roles = ', '.join(added_roles)
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles))
\ No newline at end of file
self.assertTrue(response.content.find('<td>{0}</td>'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles))
......@@ -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