diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index e7b207ca28..87e5bc04c1 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -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__ = () diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index e0e5c1a46f..ad908a2cb1 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -14,11 +14,27 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the 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() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 0a62849d8d..a7f0a71a59 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,12 +1,13 @@ 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: diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 50b536d444..8259507617 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -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 diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py new file mode 100644 index 0000000000..6890a6df2a --- /dev/null +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -0,0 +1,134 @@ +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}) diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py new file mode 100644 index 0000000000..9515e449f9 --- /dev/null +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -0,0 +1,72 @@ +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) diff --git a/lms/djangoapps/courseware/tests/test_draft_modulestore.py b/lms/djangoapps/courseware/tests/test_draft_modulestore.py new file mode 100644 index 0000000000..db6d4c45b5 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_draft_modulestore.py @@ -0,0 +1,21 @@ +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) diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 47d437a316..0a4dada18e 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -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 diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py new file mode 100644 index 0000000000..dd1f00711c --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -0,0 +1,100 @@ +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'})) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py new file mode 100644 index 0000000000..055c860fcc --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -0,0 +1,374 @@ +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')) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index e862ed62c3..43b190c04b 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,298 +1,58 @@ ''' 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) - - # ============ 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={}): - """ - 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 %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp +from helpers import LoginEnrollmentTestCase +from modulestore_config import TEST_DATA_DIR, TEST_DATA_XML_MODULESTORE, TEST_DATA_MONGO_MODULESTORE, TEST_DATA_DRAFT_MONGO_MODULESTORE class ActivateLoginTest(LoginEnrollmentTestCase): - '''Test logging in and logging out''' + """ + 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 - ''' - # enroll in the course before trying to access pages + """ + 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') diff --git a/lms/djangoapps/instructor/tests/test_download_csv.py b/lms/djangoapps/instructor/tests/test_download_csv.py index 29e18eee4d..fd5bd562ba 100644 --- a/lms/djangoapps/instructor/tests/test_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_download_csv.py @@ -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) diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 3b5bdc2ce9..5bf2afc9ea 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -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 diff --git a/lms/djangoapps/instructor/tests/test_forum_admin.py b/lms/djangoapps/instructor/tests/test_forum_admin.py index 7b4e729867..90dadd569e 100644 --- a/lms/djangoapps/instructor/tests/test_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_forum_admin.py @@ -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('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) \ No newline at end of file + self.assertTrue(response.content.find('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 99b8b1a929..a46b4b12fe 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -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)) diff --git a/lms/urls.py b/lms/urls.py index 52a7d99aaf..3e5ffea015 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -37,7 +37,7 @@ urlpatterns = ('', # nopep8 url(r'^login_ajax$', 'student.views.login_user', name="login"), url(r'^login_ajax/(?P[^/]*)$', '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[^/]*)$', 'student.views.activate_account', name="activate"), url(r'^begin_exam_registration/(?P[^/]+/[^/]+/[^/]+)$', 'student.views.begin_exam_registration', name="begin_exam_registration"),