From a25a0d71aca9892e6f67f77917f72fc1552528ba Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Wed, 12 Jun 2013 10:42:52 -0400 Subject: [PATCH 001/440] refactored tests in courseware to draw course info from mongo instead of xmlmodulestore --- .../xmodule/xmodule/modulestore/__init__.py | 4 +- lms/djangoapps/courseware/tests/tests.py | 278 ++++++++++++++---- 2 files changed, 216 insertions(+), 66 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 33c7b61251..737d83ad7c 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/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index ec3e55b1b8..97bff38341 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -31,6 +31,10 @@ from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml import XMLModuleStore +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + + log = logging.getLogger("mitx." + __name__) @@ -117,8 +121,121 @@ 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 MongoLoginHelpers(ModuleStoreTestCase): + + 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 _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 enroll(self, course): + """Enroll the currently logged-in user, and check that it worked.""" + result = self.try_enroll(course) + self.assertTrue(result) + + 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 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 + class LoginEnrollmentTestCase(TestCase): + ''' Base TestCase providing support for user creation, activation, login, and course enrollment @@ -403,19 +520,35 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): self.assertGreater(len(course.textbooks), 0) - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestNavigation(LoginEnrollmentTestCase): +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestNavigation(MongoLoginHelpers): +#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") + #self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + #self.toy = modulestore().get_course("edX/toy/2012_Fall") + self.course = CourseFactory.create() + self.full = CourseFactory.create(display_name = 'RoboboboboBOT') - # Create two accounts + 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 two accounts self.student = 'view@test.com' self.student2 = 'view2@test.com' self.password = 'foo' @@ -427,42 +560,43 @@ class TestNavigation(LoginEnrollmentTestCase): 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.course) self.enroll(self.full) # First request should redirect to ToyVideos + resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) + kwargs={'course_id': self.course.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, + 'courseware_section', kwargs={'course_id': self.course.id, 'chapter': 'Overview', - 'section': 'Toy_Videos'})) + 'section': 'Welcome'})) # Hitting the couseware tab again should # redirect to the first chapter: 'Overview' resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) + kwargs={'course_id': self.course.id})) self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.toy.id, + kwargs={'course_id': self.course.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'})) + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter', + 'section': 'factory_section'})) # And now hitting the courseware tab should redirect to 'secret:magic' resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) + kwargs={'course_id': self.course.id})) self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.toy.id, - 'chapter': 'secret:magic'})) + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter'})) @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) @@ -478,17 +612,31 @@ class TestDraftModuleStore(TestCase): # 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): +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestViewAuth(MongoLoginHelpers): """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") + self.full = CourseFactory.create(display_name='Robot_Sub_Course') + + self.course = CourseFactory.create() + self.overview_chapter = ItemFactory.create(display_name='Overview') + + self.progress_chapter = ItemFactory.create(parent_location=self.course.location, + display_name='progress') + + self.info_chapter = ItemFactory.create(parent_location=self.course.location, + display_name='info') + + self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, + display_name='Welcome') + + self.somewhere_in_progress = ItemFactory.create(parent_location=self.progress_chapter.location, + display_name='1') + # Create two accounts self.student = 'view@test.com' self.instructor = 'view2@test.com' @@ -507,21 +655,22 @@ class TestViewAuth(LoginEnrollmentTestCase): self.login(self.student, self.password) # shouldn't work before enroll response = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) + kwargs={'course_id': self.course.id})) self.assertRedirectsNoFollow(response, - reverse('about_course', - args=[self.toy.id])) - self.enroll(self.toy) + reverse('about_course', + args=[self.course.id])) + self.enroll(self.course) self.enroll(self.full) # should work now -- redirect to first page response = self.client.get(reverse('courseware', - kwargs={'course_id': self.toy.id})) + kwargs={'course_id': self.course.id})) self.assertRedirectsNoFollow(response, reverse('courseware_section', - kwargs={'course_id': self.toy.id, + kwargs={'course_id': self.course.id, 'chapter': 'Overview', - 'section': 'Toy_Videos'})) + 'section': 'Welcome'})) + def instructor_urls(course): "list of urls that only instructors/staff should be able to see" @@ -536,15 +685,15 @@ class TestViewAuth(LoginEnrollmentTestCase): return urls # Randomly sample an instructor page - url = random.choice(instructor_urls(self.toy) + - instructor_urls(self.full)) + url = random.choice(instructor_urls(self.course) + + 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_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) group.user_set.add(get_user(self.instructor)) @@ -552,7 +701,7 @@ class TestViewAuth(LoginEnrollmentTestCase): 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)) + url = random.choice(instructor_urls(self.course)) print 'checking for 200 on {0}'.format(url) self.check_for_get_code(200, url) @@ -566,8 +715,9 @@ class TestViewAuth(LoginEnrollmentTestCase): instructor.save() # and now should be able to load both - url = random.choice(instructor_urls(self.toy) + - instructor_urls(self.full)) + url = random.choice(instructor_urls(self.course) + + instructor_urls(self.full)) + print 'checking for 200 on {0}'.format(url) self.check_for_get_code(200, url) @@ -580,11 +730,11 @@ class TestViewAuth(LoginEnrollmentTestCase): """ oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] - try: - settings.MITX_FEATURES['DISABLE_START_DATES'] = False - test() - finally: - settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD + # 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 @@ -604,10 +754,10 @@ class TestViewAuth(LoginEnrollmentTestCase): # Make courses start in the future tomorrow = time.time() + 24 * 3600 - self.toy.lms.start = time.gmtime(tomorrow) + self.course.lms.start = time.gmtime(tomorrow) self.full.lms.start = time.gmtime(tomorrow) - self.assertFalse(self.toy.has_started()) + self.assertFalse(self.course.has_started()) self.assertFalse(self.full.has_started()) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) @@ -691,28 +841,28 @@ class TestViewAuth(LoginEnrollmentTestCase): # First, try with an enrolled student print '=== Testing student access....' self.login(self.student, self.password) - self.enroll(self.toy) + self.enroll(self.course) 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.course) 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_name = _course_staff_group_name(self.course.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.course) self.enroll(self.full) - # should now be able to get to everything for toy course + # should now be able to get to everything for self.course check_non_staff(self.full) - check_staff(self.toy) + check_staff(self.course) print '=== Testing staff access....' # now also make the instructor staff @@ -722,7 +872,7 @@ class TestViewAuth(LoginEnrollmentTestCase): # and now should be able to load both check_staff(self.toy) - check_staff(self.full) + #check_staff(self.full) def _do_test_enrollment_period(self): """Actually do the test, relying on settings to be right.""" @@ -733,9 +883,9 @@ class TestViewAuth(LoginEnrollmentTestCase): yesterday = time.time() - 24 * 3600 print "changing" - # toy course's enrollment period hasn't started - self.toy.enrollment_start = time.gmtime(tomorrow) - self.toy.enrollment_end = time.gmtime(nextday) + # self.course's enrollment period hasn't started + self.course.enrollment_start = time.gmtime(tomorrow) + self.course.enrollment_end = time.gmtime(nextday) # full course's has self.full.enrollment_start = time.gmtime(yesterday) @@ -745,12 +895,12 @@ class TestViewAuth(LoginEnrollmentTestCase): # First, try with an enrolled student print '=== Testing student access....' self.login(self.student, self.password) - self.assertFalse(self.try_enroll(self.toy)) + self.assertFalse(self.try_enroll(self.course)) 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_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) group.user_set.add(get_user(self.instructor)) @@ -758,7 +908,7 @@ class TestViewAuth(LoginEnrollmentTestCase): 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)) + self.assertTrue(self.try_enroll(self.course)) print '=== Testing staff access....' # now make the instructor global staff, but not in the instructor group @@ -768,8 +918,8 @@ class TestViewAuth(LoginEnrollmentTestCase): instructor.save() # unenroll and try again - self.unenroll(self.toy) - self.assertTrue(self.try_enroll(self.toy)) + self.unenroll(self.course) + self.assertTrue(self.try_enroll(self.course)) def _do_test_beta_period(self): """Actually test beta periods, relying on settings to be right.""" @@ -783,23 +933,23 @@ class TestViewAuth(LoginEnrollmentTestCase): # yesterday = time.time() - 24 * 3600 # toy course's hasn't started - self.toy.lms.start = time.gmtime(tomorrow) - self.assertFalse(self.toy.has_started()) + self.course.lms.start = time.gmtime(tomorrow) + self.assertFalse(self.course.has_started()) # but should be accessible for beta testers - self.toy.lms.days_early_for_beta = 2 + self.course.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')) + 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.toy.location) + 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.toy, 'load')) + self.assertTrue(has_access(student_user, self.course, 'load')) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) From 07486a4dbc971511f742f35e5fb69866bcf87d03 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 17 Jun 2013 11:54:16 -0400 Subject: [PATCH 002/440] Refactoring the code in tests.py to remove unnecessary dependencies on the XML Modulestore. --- common/djangoapps/student/views.py | 2 +- .../xmodule/modulestore/tests/factories.py | 7 +- .../courseware/tests/mongo_login_helpers.py | 191 +++++++ .../courseware/tests/test_navigation.py | 440 +++++++++++++++ .../tests/test_view_authentication.py | 418 ++++++++++++++ lms/djangoapps/courseware/tests/tests.py | 530 +----------------- 6 files changed, 1057 insertions(+), 531 deletions(-) create mode 100644 lms/djangoapps/courseware/tests/mongo_login_helpers.py create mode 100644 lms/djangoapps/courseware/tests/test_navigation.py create mode 100644 lms/djangoapps/courseware/tests/test_view_authentication.py diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 474581c688..4ab5d95833 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -349,6 +349,7 @@ def change_enrollment(request): return HttpResponseBadRequest("Course id not specified") if action == "enroll": + # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from try: @@ -357,7 +358,6 @@ def change_enrollment(request): log.warning("User {0} tried to enroll in non-existent course {1}" .format(user.username, course_id)) return HttpResponseBadRequest("Course id is invalid") - if not has_access(user, course, 'enroll'): return HttpResponseBadRequest("Enrollment is closed") diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 8cf148f742..0d7c91cca6 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,5 +1,5 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute -from time import gmtime +from time import gmtime, time from uuid import uuid4 from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -35,7 +35,10 @@ class XModuleCourseFactory(Factory): if display_name is not None: new_course.display_name = display_name + tomorrow = time() + 24 * 3600 + new_course.lms.start = gmtime() + new_course.enrollment_start = gmtime(tomorrow) new_course.tabs = kwargs.get( 'tabs', [ @@ -55,6 +58,8 @@ class XModuleCourseFactory(Factory): if data is not None: store.update_item(new_course.location, data) + new_course = store.get_instance(new_course.id, new_course.location) + return new_course diff --git a/lms/djangoapps/courseware/tests/mongo_login_helpers.py b/lms/djangoapps/courseware/tests/mongo_login_helpers.py new file mode 100644 index 0000000000..a9eb822516 --- /dev/null +++ b/lms/djangoapps/courseware/tests/mongo_login_helpers.py @@ -0,0 +1,191 @@ +import logging +import json + +from urlparse import urlsplit, urlunsplit + +from django.contrib.auth.models import User, Group +from django.test import TestCase +from django.test.client import RequestFactory +from django.conf import settings +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +import xmodule.modulestore.django + +# 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 + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from xmodule.modulestore.mongo import MongoModuleStore +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) + + +class MongoLoginHelpers(ModuleStoreTestCase): + + 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 diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py new file mode 100644 index 0000000000..7d7406f30c --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -0,0 +1,440 @@ +import logging +import json +import random + +from urlparse import urlsplit, urlunsplit +from uuid import uuid4 + +from django.contrib.auth.models import User +from django.test import TestCase +from django.conf import settings +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +import xmodule.modulestore.django + +from student.models import Registration +from xmodule.error_module import ErrorDescriptor +from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml import XMLModuleStore + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +from mongo_login_helpers import * + +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 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 + + +class ActivateLoginTest(LoginEnrollmentTestCase): + '''Test logging in and logging out''' + def setUp(self): + self.setup_viewtest_user() + + def test_activate_login(self): + '''Test login -- the setup function does all the work''' + pass + + def test_logout(self): + '''Test logout -- setup function does login''' + self.logout() + + +class PageLoaderTestCase(LoginEnrollmentTestCase): + ''' 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 + courses = module_store.get_courses() + self.assertEqual(len(courses), 1) + course = courses[0] + self.enroll(course) + course_id = course.id + + # Search for items in the course + # None is treated as a wildcard + course_loc = course.location + location_query = Location(course_loc.tag, course_loc.org, + course_loc.course, None, None, None) + + items = module_store.get_items(location_query) + + if len(items) < 1: + self.fail('Could not retrieve any items from course') + else: + descriptor = random.choice(items) + + # We have ancillary course information now as modules + # and we can't simply use 'jump_to' to view them + if descriptor.location.category == 'about': + self._assert_loads('about_course', + {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'static_tab': + kwargs = {'course_id': course_id, + 'tab_slug': descriptor.location.name} + self._assert_loads('static_tab', kwargs, descriptor) + + elif descriptor.location.category == 'course_info': + self._assert_loads('info', {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'custom_tag_template': + pass + + else: + + kwargs = {'course_id': course_id, + 'location': descriptor.location.url()} + + self._assert_loads('jump_to', kwargs, descriptor, + expect_redirect=True, + check_content=True) + + 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) + + if response.status_code != 200: + self.fail('Status %d for page %s' % + (response.status_code, descriptor.location.url())) + + if expect_redirect: + self.assertEqual(response.redirect_chain[0][1], 302) + + if check_content: + unavailable_msg = "this module is temporarily unavailable" + self.assertEqual(response.content.find(unavailable_msg), -1) + self.assertFalse(isinstance(descriptor, ErrorDescriptor)) + + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): + '''Check that all pages in test courses load properly from XML''' + + def setUp(self): + super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() + self.setup_viewtest_user() + xmodule.modulestore.django._MODULESTORES = {} + + def test_toy_course_loads(self): + module_class = 'xmodule.hidden_module.HiddenDescriptor' + module_store = XMLModuleStore(TEST_DATA_DIR, + default_class=module_class, + course_dirs=['toy'], + load_error_modules=True) + + self.check_random_page_loads(module_store) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): + '''Check that all pages in test courses load properly from Mongo''' + + def setUp(self): + super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() + self.setup_viewtest_user() + xmodule.modulestore.django._MODULESTORES = {} + modulestore().collection.drop() + + def test_toy_course_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['toy']) + self.check_random_page_loads(module_store) + + def test_full_textbooks_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['full']) + + course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + self.assertGreater(len(course.textbooks), 0) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestNavigation(MongoLoginHelpers): + + """Check that navigation state is saved properly""" + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.course = CourseFactory.create() + self.full = CourseFactory.create(display_name='Robot_Sub_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 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.course) + self.enroll(self.full) + + # First request should redirect to ToyVideos + + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.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.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + # Hitting the couseware tab again should + # redirect to the first chapter: 'Overview' + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.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.course.id, + 'chapter': 'factory_chapter', + 'section': 'factory_section'})) + + # And now hitting the courseware tab should redirect to 'secret:magic' + resp = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + + self.assertRedirectsNoFollow(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..842641f14f --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -0,0 +1,418 @@ +import logging +import time +import datetime +import pytz +import random + +from uuid import uuid4 + +from django.contrib.auth.models import User, Group +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.access import (has_access, _course_staff_group_name, + course_beta_test_group_name) + +from mongo_login_helpers import MongoLoginHelpers + +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +log = logging.getLogger("mitx." + __name__) + + +def get_user(email): + '''look up a user by email''' + return User.objects.get(email=email) + + +def update_course(course, data): + """ + Updates the version of course in the mongo modulestore + with the metadata in data and returns the updated version. + """ + + store = xmodule.modulestore.django.modulestore() + + store.update_item(course.location, data) + + store.update_metadata(course.location, data) + + updated_course = store.get_instance(course.id, course.location) + + return updated_course + + +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 + + +TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT +TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestViewAuth(MongoLoginHelpers): + """Check that view authentication works properly""" + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.full = CourseFactory.create(display_name='Robot_Sub_Course') + self.course = CourseFactory.create() + + self.overview_chapter = ItemFactory.create(display_name='Overview') + self.progress_chapter = ItemFactory.create(parent_location=self.course.location, + display_name='progress') + self.info_chapter = ItemFactory.create(parent_location=self.course.location, + display_name='info') + self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, + display_name='Welcome') + self.somewhere_in_progress = ItemFactory.create(parent_location=self.progress_chapter.location, + display_name='1') + + # 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.course.id})) + + self.assertRedirectsNoFollow(response, + reverse('about_course', + args=[self.course.id])) + self.enroll(self.course) + self.enroll(self.full) + # should work now -- redirect to first page + response = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + self.assertRedirectsNoFollow(response, + reverse('courseware_section', + kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + 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.course) + + 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.course.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.course)) + 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.course) + + instructor_urls(self.full)) + + print 'checking for 200 on {0}'.format(url) + self.check_for_get_code(200, url) + + def run_wrapped(self, test): + """ + test.py turns off start dates. Enable them. + Because settings is global, be careful not to mess it up for other tests + (Can't use override_settings because we're only changing part of the + MITX_FEATURES dict) + """ + oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] + + try: + settings.MITX_FEATURES['DISABLE_START_DATES'] = False + test() + finally: + settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD + + def test_dark_launch(self): + """Make sure that before course start, students can't access course + pages, but instructors can""" + self.run_wrapped(self._do_test_dark_launch) + + def test_enrollment_period(self): + """Check that enrollment periods work""" + self.run_wrapped(self._do_test_enrollment_period) + + def test_beta_period(self): + """Check that beta-test access works""" + self.run_wrapped(self._do_test_beta_period) + + def _do_test_dark_launch(self): + """Actually do the test, relying on settings to be right.""" + + # Make courses start in the future + tomorrow = time.time() + 24 * 3600 + self.course.start = time.gmtime(tomorrow) + self.full.start = time.gmtime(tomorrow) + + self.assertFalse(self.course.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.course) + self.enroll(self.full) + + # shouldn't be able to get to anything except the light pages + check_non_staff(self.course) + 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.course.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.course) + self.enroll(self.full) + + # should now be able to get to everything for self.course + check_non_staff(self.full) + check_staff(self.course) + + 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.course) + 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 + 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} + full_data = {'enrollment_start': yesterday, 'enrollment_end': tomorrow} + + print "changing" + # self.course's enrollment period hasn't started + print self.course.enrollment_start + self.course = update_course(self.course, course_data) + print 'FLAG' + print self.course.enrollment_start + print 'FLAG' + #self.course.enrollment_start = time.gmtime(tomorrow) + #self.course.enrollment_end = time.gmtime(nextday) + # full course's has + print self.full.enrollment_start + self.full = update_course(self.full, full_data) + print self.full.enrollment_start + #self.full.enrollment_start = time.gmtime(yesterday) + #self.full.enrollment_end = time.gmtime(tomorrow) + + print "login" + # First, try with an enrolled student + print '=== Testing student access....' + self.login(self.student, self.password) + self.assertFalse(self.try_enroll(self.course)) + 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.course.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.course)) + + 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.course) + self.assertTrue(self.try_enroll(self.course)) + + def _do_test_beta_period(self): + """Actually test beta periods, relying on settings to be right.""" + + # trust, but verify :) + self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) + + # Make courses start in the future + tomorrow = time.time() + 24 * 3600 + # nextday = tomorrow + 24 * 3600 + # yesterday = time.time() - 24 * 3600 + + # toy course's hasn't started + self.course.lms.start = time.gmtime(tomorrow) + 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 = get_user(self.student) + 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 97bff38341..2c570e6966 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -121,118 +121,6 @@ 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 MongoLoginHelpers(ModuleStoreTestCase): - - 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 _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 enroll(self, course): - """Enroll the currently logged-in user, and check that it worked.""" - result = self.try_enroll(course) - self.assertTrue(result) - - 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 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 - class LoginEnrollmentTestCase(TestCase): @@ -520,84 +408,6 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): self.assertGreater(len(course.textbooks), 0) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestNavigation(MongoLoginHelpers): -#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") - self.course = CourseFactory.create() - self.full = CourseFactory.create(display_name = 'RoboboboboBOT') - - 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 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.course) - self.enroll(self.full) - - # First request should redirect to ToyVideos - - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.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.course.id, - 'chapter': 'Overview', - 'section': 'Welcome'})) - - # Hitting the couseware tab again should - # redirect to the first chapter: 'Overview' - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.id})) - - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.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.course.id, - 'chapter': 'factory_chapter', - 'section': 'factory_section'})) - - # And now hitting the courseware tab should redirect to 'secret:magic' - resp = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.id})) - - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.id, - 'chapter': 'factory_chapter'})) - @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) class TestDraftModuleStore(TestCase): @@ -612,345 +422,6 @@ class TestDraftModuleStore(TestCase): # The bug was that 'course_id' argument was # not allowed to be passed in (i.e. was throwing exception) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestViewAuth(MongoLoginHelpers): - """Check that view authentication works properly""" - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - self.full = CourseFactory.create(display_name='Robot_Sub_Course') - - self.course = CourseFactory.create() - - self.overview_chapter = ItemFactory.create(display_name='Overview') - - self.progress_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='progress') - - self.info_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='info') - - self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, - display_name='Welcome') - - self.somewhere_in_progress = ItemFactory.create(parent_location=self.progress_chapter.location, - display_name='1') - - # 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.course.id})) - - self.assertRedirectsNoFollow(response, - reverse('about_course', - args=[self.course.id])) - self.enroll(self.course) - self.enroll(self.full) - # should work now -- redirect to first page - response = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.id})) - self.assertRedirectsNoFollow(response, - reverse('courseware_section', - kwargs={'course_id': self.course.id, - 'chapter': 'Overview', - 'section': 'Welcome'})) - - - 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.course) + - 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.course.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.course)) - 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.course) + - instructor_urls(self.full)) - - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) - - def run_wrapped(self, test): - """ - test.py turns off start dates. Enable them. - Because settings is global, be careful not to mess it up for other tests - (Can't use override_settings because we're only changing part of the - MITX_FEATURES dict) - """ - oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] - - # try: - # settings.MITX_FEATURES['DISABLE_START_DATES'] = False - # test() - # finally: - settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD - - def test_dark_launch(self): - """Make sure that before course start, students can't access course - pages, but instructors can""" - self.run_wrapped(self._do_test_dark_launch) - - def test_enrollment_period(self): - """Check that enrollment periods work""" - self.run_wrapped(self._do_test_enrollment_period) - - def test_beta_period(self): - """Check that beta-test access works""" - self.run_wrapped(self._do_test_beta_period) - - def _do_test_dark_launch(self): - """Actually do the test, relying on settings to be right.""" - - # Make courses start in the future - tomorrow = time.time() + 24 * 3600 - self.course.lms.start = time.gmtime(tomorrow) - self.full.lms.start = time.gmtime(tomorrow) - - self.assertFalse(self.course.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.course) - self.enroll(self.full) - - # shouldn't be able to get to anything except the light pages - check_non_staff(self.course) - 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.course.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.course) - self.enroll(self.full) - - # should now be able to get to everything for self.course - check_non_staff(self.full) - check_staff(self.course) - - 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 = time.time() + 24 * 3600 - nextday = tomorrow + 24 * 3600 - yesterday = time.time() - 24 * 3600 - - print "changing" - # self.course's enrollment period hasn't started - self.course.enrollment_start = time.gmtime(tomorrow) - self.course.enrollment_end = time.gmtime(nextday) - - # full course's has - self.full.enrollment_start = time.gmtime(yesterday) - self.full.enrollment_end = time.gmtime(tomorrow) - - print "login" - # First, try with an enrolled student - print '=== Testing student access....' - self.login(self.student, self.password) - self.assertFalse(self.try_enroll(self.course)) - 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.course.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.course)) - - 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.course) - self.assertTrue(self.try_enroll(self.course)) - - def _do_test_beta_period(self): - """Actually test beta periods, relying on settings to be right.""" - - # trust, but verify :) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - - # Make courses start in the future - tomorrow = time.time() + 24 * 3600 - # nextday = tomorrow + 24 * 3600 - # yesterday = time.time() - 24 * 3600 - - # toy course's hasn't started - self.course.lms.start = time.gmtime(tomorrow) - 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 = get_user(self.student) - 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')) - @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestSubmittingProblems(LoginEnrollmentTestCase): @@ -1006,6 +477,7 @@ class TestSubmittingProblems(LoginEnrollmentTestCase): 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): From 97275a328634649d3bf2a41a819e2c931346caa7 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Tue, 18 Jun 2013 11:53:43 -0400 Subject: [PATCH 003/440] Refactored some of the classes in tests.py to not unnecessarily depend on the XML modulestore. Conflicts: lms/djangoapps/courseware/tests/test_navigation.py lms/djangoapps/courseware/tests/test_view_authentication.py lms/djangoapps/courseware/tests/tests.py --- .../xmodule/modulestore/tests/factories.py | 5 +- lms/djangoapps/courseware/access.py | 5 +- .../courseware/tests/test_navigation.py | 1 - .../tests/test_view_authentication.py | 17 +- lms/djangoapps/courseware/tests/tests.py | 459 ------------------ 5 files changed, 17 insertions(+), 470 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 247122fa31..b91e9be700 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 @@ -149,6 +150,8 @@ class XModuleItemFactory(Factory): if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) + new_item = store.get_item(new_item.location) + return new_item diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 07987a8edf..9e6a371552 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -245,7 +245,8 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): if descriptor.lms.start is not None: now = datetime.now(UTC()) effective_start = _adjust_start_date_for_beta_testers(user, descriptor) - if now > effective_start: + difference = (now - effective_start).total_seconds() + if difference > 3600: # after start date, everyone can see it debug("Allow: now > effective start date") return True @@ -508,6 +509,7 @@ def _adjust_start_date_for_beta_testers(user, descriptor): start_as_datetime = descriptor.lms.start delta = timedelta(descriptor.lms.days_early_for_beta) effective = start_as_datetime - delta + # ...and back to time_struct return effective @@ -570,7 +572,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/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 7d7406f30c..242379d8ca 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -80,7 +80,6 @@ def xml_store_config(data_dir): 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): diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index e739c25c93..da4f40e0db 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -1,5 +1,4 @@ import logging -import time import datetime import pytz import random @@ -80,10 +79,14 @@ class TestViewAuth(MongoLoginHelpers): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - self.full = CourseFactory.create(display_name='Robot_Sub_Course') + self.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') self.course = CourseFactory.create() - self.overview_chapter = ItemFactory.create(display_name='Overview') + self.courseware_chapter = ItemFactory.create(display_name='courseware') + self.sub_courseware_chapter = ItemFactory.create(parent_location=self.full.location, + display_name='courseware') + self.sub_overview_chapter = ItemFactory.create(parent_location=self.sub_courseware_chapter.location, + display_name='Overview') self.progress_chapter = ItemFactory.create(parent_location=self.course.location, display_name='progress') self.info_chapter = ItemFactory.create(parent_location=self.course.location, @@ -121,6 +124,7 @@ class TestViewAuth(MongoLoginHelpers): # should work now -- redirect to first page response = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) + self.assertRedirectsNoFollow(response, reverse('courseware_section', kwargs={'course_id': self.course.id, @@ -210,8 +214,8 @@ class TestViewAuth(MongoLoginHelpers): # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) - self.course.start = tomorrow - self.full.start = tomorrow + self.course.lms.start = tomorrow + self.full.lms.start = tomorrow self.assertFalse(self.course.has_started()) self.assertFalse(self.full.has_started()) @@ -344,7 +348,6 @@ class TestViewAuth(MongoLoginHelpers): print "changing" # self.course's enrollment period hasn't started - print self.course.enrollment_start self.course = update_course(self.course, course_data) # full course's has self.full = update_course(self.full, full_data) @@ -391,7 +394,7 @@ class TestViewAuth(MongoLoginHelpers): # nextday = tomorrow + 24 * 3600 # yesterday = time.time() - 24 * 3600 - # toy course's hasn't started + # self.course's hasn't started self.course.lms.start = tomorrow self.assertFalse(self.course.has_started()) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index c6e5a5f5b7..d7883d88d3 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -272,144 +272,6 @@ class LoginEnrollmentTestCase(TestCase): return resp -class ActivateLoginTest(LoginEnrollmentTestCase): - '''Test logging in and logging out''' - def setUp(self): - self.setup_viewtest_user() - - def test_activate_login(self): - '''Test login -- the setup function does all the work''' - pass - - def test_logout(self): - '''Test logout -- setup function does login''' - self.logout() - - -class PageLoaderTestCase(LoginEnrollmentTestCase): - ''' 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 - courses = module_store.get_courses() - self.assertEqual(len(courses), 1) - course = courses[0] - self.enroll(course) - course_id = course.id - - # Search for items in the course - # None is treated as a wildcard - course_loc = course.location - location_query = Location(course_loc.tag, course_loc.org, - course_loc.course, None, None, None) - - items = module_store.get_items(location_query) - - if len(items) < 1: - self.fail('Could not retrieve any items from course') - else: - descriptor = random.choice(items) - - # We have ancillary course information now as modules - # and we can't simply use 'jump_to' to view them - if descriptor.location.category == 'about': - self._assert_loads('about_course', - {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'static_tab': - kwargs = {'course_id': course_id, - 'tab_slug': descriptor.location.name} - self._assert_loads('static_tab', kwargs, descriptor) - - elif descriptor.location.category == 'course_info': - self._assert_loads('info', {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'custom_tag_template': - pass - - else: - - kwargs = {'course_id': course_id, - 'location': descriptor.location.url()} - - self._assert_loads('jump_to', kwargs, descriptor, - expect_redirect=True, - check_content=True) - - 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) - - if response.status_code != 200: - self.fail('Status %d for page %s' % - (response.status_code, descriptor.location.url())) - - if expect_redirect: - self.assertEqual(response.redirect_chain[0][1], 302) - - if check_content: - unavailable_msg = "this module is temporarily unavailable" - self.assertEqual(response.content.find(unavailable_msg), -1) - self.assertFalse(isinstance(descriptor, ErrorDescriptor)) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from XML''' - - def setUp(self): - super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - - def test_toy_course_loads(self): - module_class = 'xmodule.hidden_module.HiddenDescriptor' - module_store = XMLModuleStore(TEST_DATA_DIR, - default_class=module_class, - course_dirs=['toy'], - load_error_modules=True) - - self.check_random_page_loads(module_store) - - -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from Mongo''' - - def setUp(self): - super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - modulestore().collection.drop() - - def test_toy_course_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['toy']) - self.check_random_page_loads(module_store) - - def test_full_textbooks_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['full']) - - course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) - - self.assertGreater(len(course.textbooks), 0) - - @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) class TestDraftModuleStore(TestCase): def test_get_items_with_course_items(self): @@ -424,327 +286,6 @@ class TestDraftModuleStore(TestCase): # 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""" From 905d884aa7a517bdc11cde348f5b8fb76ccef093 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Tue, 18 Jun 2013 15:41:53 -0400 Subject: [PATCH 004/440] Removed a unnecessary imports from mongo_login_helpers.py and repeated code from tests.py. --- .../courseware/tests/mongo_login_helpers.py | 21 +--- lms/djangoapps/courseware/tests/tests.py | 104 ------------------ 2 files changed, 1 insertion(+), 124 deletions(-) diff --git a/lms/djangoapps/courseware/tests/mongo_login_helpers.py b/lms/djangoapps/courseware/tests/mongo_login_helpers.py index a9eb822516..a329f71d13 100644 --- a/lms/djangoapps/courseware/tests/mongo_login_helpers.py +++ b/lms/djangoapps/courseware/tests/mongo_login_helpers.py @@ -3,32 +3,13 @@ import json from urlparse import urlsplit, urlunsplit -from django.contrib.auth.models import User, Group -from django.test import TestCase -from django.test.client import RequestFactory -from django.conf import settings +from django.contrib.auth.models import User 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 -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.mongo import MongoModuleStore log = logging.getLogger("mitx." + __name__) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index d7883d88d3..3e39227171 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -351,110 +351,6 @@ class TestSubmittingProblems(LoginEnrollmentTestCase): 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.""" From e08215e62aa2938d767b4058bc7f5c4aed0d429c Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 15 May 2013 14:05:31 -0400 Subject: [PATCH 005/440] JSinput input type --- common/lib/capa/capa/inputtypes.py | 62 ++++++ common/lib/capa/capa/responsetypes.py | 2 +- common/lib/capa/capa/templates/jsinput.html | 64 ++++++ .../xmodule/js/src/capa/display.coffee | 5 + common/static/css/capa/jsinput_css.css | 0 common/static/js/capa/jsinput.js | 197 ++++++++++++++++++ common/static/js/test/jsinput/jsinput.js | 16 ++ .../static/js/test/jsinput/mainfixture.html | 103 +++++++++ .../src/pip-delete-this-directory.txt | 5 + 9 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 common/lib/capa/capa/templates/jsinput.html create mode 100644 common/static/css/capa/jsinput_css.css create mode 100644 common/static/js/capa/jsinput.js create mode 100644 common/static/js/test/jsinput/jsinput.js create mode 100644 common/static/js/test/jsinput/mainfixture.html create mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 65280d6d29..963062a263 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -451,6 +451,68 @@ class JavascriptInput(InputTypeBase): registry.register(JavascriptInput) + +#----------------------------------------------------------------------------- + + +class JSInput(InputTypeBase): + """ + DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN + BACKWARDS-INCOMPATIBLE WAYS. + Inputtype for general javascript inputs. Intended to be used with + customresponse. + Loads in a sandboxed iframe to help prevent css and js conflicts between + frame and top-level window. + + iframe sandbox whitelist: + - allow-scripts + - allow-popups + - allow-forms + - allow-pointer-lock + + This in turn means that the iframe cannot directly access the top-level + window elements. + Example: + + + + See the documentation in the /doc/public folder for more information. + """ + + template = "jsinput.html" + tags = ['jsinput'] + + @classmethod + def get_attributes(cls): + """ + Register the attributes. + """ + return [Attribute('params', None), # extra iframe params + Attribute('html_file', None), + Attribute('gradefn', "gradefn"), + Attribute('get_statefn', None), # Function to call in iframe + # to get current state. + Attribute('set_statefn', None), # Function to call iframe to + # set state + Attribute('width', "400"), # iframe width + Attribute('height', "300")] # iframe height + + + + def _extra_context(self): + context = { + 'applet_loader': '/static/js/capa/jsinput.js', + 'saved_state': self.value + } + + return context + + + +registry.register(JSInput) #----------------------------------------------------------------------------- class TextLine(InputTypeBase): diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..f5c15260de 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -929,7 +929,7 @@ class CustomResponse(LoncapaResponse): 'chemicalequationinput', 'vsepr_input', 'drag_and_drop_input', 'editamoleculeinput', 'designprotein2dinput', 'editageneinput', - 'annotationinput'] + 'annotationinput', 'jsinput'] def setup_response(self): xml = self.xml diff --git a/common/lib/capa/capa/templates/jsinput.html b/common/lib/capa/capa/templates/jsinput.html new file mode 100644 index 0000000000..ec5d32b5c2 --- /dev/null +++ b/common/lib/capa/capa/templates/jsinput.html @@ -0,0 +1,64 @@ +
+ + +
+ % if status == 'unsubmitted': +
+ % elif status == 'correct': +
+ % elif status == 'incorrect': +
+ % elif status == 'incomplete': +
+ % endif + + + + +
+ +

+ +

+

+

+ + + +
+
+
+ +
+ + + +
+ +

+ +

+

+

+ + + +
+
+ + diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt new file mode 100644 index 0000000000..c8883ea99f --- /dev/null +++ b/requirements/src/pip-delete-this-directory.txt @@ -0,0 +1,5 @@ +This file is placed here by pip to indicate the source was put +here by pip. + +Once this package is successfully installed this source code will be +deleted (unless you remove this file). From 3c55a1e95d0bc912d457848f097e15590ae19760 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:35:10 -0400 Subject: [PATCH 006/440] Add sphinx documentation for jsinput --- doc/public/course_data_formats/jsinput.rst | 143 +++++++++++++++++++++ doc/public/index.rst | 1 + 2 files changed, 144 insertions(+) create mode 100644 doc/public/course_data_formats/jsinput.rst diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst new file mode 100644 index 0000000000..5252a5dd0c --- /dev/null +++ b/doc/public/course_data_formats/jsinput.rst @@ -0,0 +1,143 @@ +############################################################################## +JS Input +############################################################################## + + **NOTE** + *Do not use this feature yet! Its attributes and behaviors may change + without any concern for backwards compatibility. Moreover, it has only been + tested in a very limited context. If you absolutely must, contact Julian + (julian@edx.org). When the feature stabilizes, this note will be removed.* + +This document explains how to write a JSInput input type. JSInput is meant to +allow problem authors to easily turn working standalone HTML files into +problems that can be integrated into the edX platform. Since it's aim is +flexibility, it can be seen as the input and client-side equivalent of +CustomResponse. + +A JSInput input creates an iframe into a static HTML page, and passes the +return value of author-specified functions to the enclosing response type +(generally CustomResponse). JSInput can also stored and retrieve state. + +****************************************************************************** +Format +****************************************************************************** + +A jsinput problem looks like this: + +.. code-block:: xml + + + + + + + + +The accepted attributes are: + +============== ============== ========= ========== +Attribute Name Value Type Required? Default +============== ============== ========= ========== +html_file Url string Yes None +gradefn Function name Yes `gradefn` +set_statefn Function name No None +get_statefn Function name No None +height Integer No `500` +width Integer No `400` +============== ============== ========= ========== + +****************************************************************************** +Required Attributes +****************************************************************************** + +============================================================================== +html_file +============================================================================== + +The `html_file` attribute specifies what html file the iframe will point to. This +should be located in the content directory. + +The iframe is created using the sandbox attribute; while popups, scripts, and +pointer locks are allowed, the iframe cannot access its parent's attributes. + +The html file should contain a top-level function for the gradefn function. To +check whether the gradefn will be accessible to JSInput, check that, in the +console,:: + window["`gradefn`"] +Returns the right thing. + +Aside from that, more or less anything goes. Note that currently there is no +support for inheriting css or javascript from the parent (aside from the +Chrome-only `seamless` attribute, which is set to true by default). + +============================================================================== +gradefn +============================================================================== + +The `gradefn` attribute specifies the name of the function that will be called +when a user clicks on the "Check" button, and which should return the student's +answer. This answer will (unless both the get_statefn and set_statefn +attributes are also used) be passed as a string to the enclosing response type. +In the customresponse example above, this means cfn will be passed this answer +as `ans`. + +**IMPORTANT** : the `gradefn` function should not be at all asynchronous, since +this could result in the student's latest answer not being passed correctly. +Moreover, the function should also return promptly, since currently the student +has no indication that her answer is being calculated/produced. + +****************************************************************************** +Option Attributes +****************************************************************************** + +The `height` and `width` attributes are straightforward: they specify the +height and width of the iframe. Both are limited by the enclosing DOM elements, +so for instance there is an implicit max-width of around 900. + +In the future, JSInput may attempt to make these dimensions match the html +file's dimensions (up to the aforementioned limits), but currently it defaults +to `500` and `400` for `height` and `width`, respectively. + +============================================================================== +set_statefn +============================================================================== + +Sometimes a problem author will want information about a student's previous +answers ("state") to be saved and reloaded. If the attribute `set_statefn` is +used, the function given as its value will be passed the state as a string +argument whenever there is a state, and the student returns to a problem. It is +the responsibility of the function to then use this state approriately. + +The state that is passed is: + +1. The previous output of `gradefn` (i.e., the previous answer) if + `get_statefn` is not defined. +2. The previous output of `get_statefn` (see below) otherwise. + +It is the responsibility of the iframe to do proper verification of the +argument that it receives via `set_statefn`. + +============================================================================== +get_statefn +============================================================================== + +Sometimes the state and the answer are quite different. For instance, a problem +that involves using a javascript program that allows the student to alter a +molecule may grade based on the molecule's hidrophobicity, but from the +hidrophobicity it might be incapable of restoring the state. In that case, a +*separate* state may be stored and loaded by `set_statefn`. Note that if +`get_statefn` is defined, the answer (i.e., what is passed to the enclosing +response type) will be a json string with the following format:: + { + answer: `[answer string]` + state: `[state string]` + } + +It is the responsibility of the enclosing response type to then parse this as +json. diff --git a/doc/public/index.rst b/doc/public/index.rst index 064b3ff443..abc9978aeb 100644 --- a/doc/public/index.rst +++ b/doc/public/index.rst @@ -28,6 +28,7 @@ Specific Problem Types course_data_formats/conditional_module/conditional_module.rst course_data_formats/word_cloud/word_cloud.rst course_data_formats/custom_response.rst + course_data_formats/jsinput.rst Internal Data Formats From 49fba9e84d757fef6e444f7003eac3634cea5194 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:54:28 -0400 Subject: [PATCH 007/440] Remove obnoxious pip file --- requirements/src/pip-delete-this-directory.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt deleted file mode 100644 index c8883ea99f..0000000000 --- a/requirements/src/pip-delete-this-directory.txt +++ /dev/null @@ -1,5 +0,0 @@ -This file is placed here by pip to indicate the source was put -here by pip. - -Once this package is successfully installed this source code will be -deleted (unless you remove this file). From 9bfddd4891281dfeeb37b2d596c41627908813e2 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Fri, 21 Jun 2013 14:05:57 -0400 Subject: [PATCH 008/440] Addressed pull request feedback. --- .../xmodule/modulestore/tests/django_utils.py | 16 + .../xmodule/modulestore/tests/factories.py | 4 + lms/djangoapps/courseware/access.py | 3 +- .../courseware/tests/check_request_code.py | 24 + lms/djangoapps/courseware/tests/helpers.py | 142 +++++ .../courseware/tests/modulestore_config.py | 72 +++ .../courseware/tests/mongo_login_helpers.py | 172 ------ .../tests/test_draft_modulestore.py | 21 + .../courseware/tests/test_masquerade.py | 7 +- .../courseware/tests/test_navigation.py | 449 ++------------- .../tests/test_view_authentication.py | 374 ++++++------- lms/djangoapps/courseware/tests/tests.py | 516 +++++------------- .../instructor/tests/test_download_csv.py | 9 +- .../instructor/tests/test_enrollment.py | 5 +- .../instructor/tests/test_forum_admin.py | 9 +- lms/djangoapps/open_ended_grading/tests.py | 19 +- lms/urls.py | 2 +- 17 files changed, 694 insertions(+), 1150 deletions(-) create mode 100644 lms/djangoapps/courseware/tests/check_request_code.py create mode 100644 lms/djangoapps/courseware/tests/helpers.py create mode 100644 lms/djangoapps/courseware/tests/modulestore_config.py delete mode 100644 lms/djangoapps/courseware/tests/mongo_login_helpers.py create mode 100644 lms/djangoapps/courseware/tests/test_draft_modulestore.py diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 04e79ce521..944b9e5bd4 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -14,6 +14,22 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the TestCase and drops it they are finished. """ + def update_course(self, course, data): + """ + Updates the version of course in the mongo modulestore + with the metadata in data and returns the updated version. + """ + + store = xmodule.modulestore.django.modulestore() + + store.update_item(course.location, data) + + store.update_metadata(course.location, data) + + updated_course = store.get_instance(course.id, course.location) + + return updated_course + @staticmethod def 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 b91e9be700..4f63fbc1d2 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -60,6 +60,8 @@ 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 @@ -150,6 +152,8 @@ 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 diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 9e6a371552..eb732311cf 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -245,8 +245,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): if descriptor.lms.start is not None: now = datetime.now(UTC()) effective_start = _adjust_start_date_for_beta_testers(user, descriptor) - difference = (now - effective_start).total_seconds() - if difference > 3600: + if now > effective_start: # after start date, everyone can see it debug("Allow: now > effective start date") return True diff --git a/lms/djangoapps/courseware/tests/check_request_code.py b/lms/djangoapps/courseware/tests/check_request_code.py new file mode 100644 index 0000000000..1393d2fe17 --- /dev/null +++ b/lms/djangoapps/courseware/tests/check_request_code.py @@ -0,0 +1,24 @@ + + +def check_for_get_code(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(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 diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py new file mode 100644 index 0000000000..99da5e9061 --- /dev/null +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -0,0 +1,142 @@ +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. + """ + 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. + """ + 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): + + 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 = 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 it worked. + Returns an HTTP response which e + """ + resp = self.client.get(reverse('logout'), {}) + # should redirect + self.assertEqual(resp.status_code, 302) + + def create_account(self, username, email, password): + """ + Create the account and check that it worked. + """ + resp = self.client.post(reverse('create_account'), { + 'username': username, + 'email': email, + 'password': password, + 'name': 'username', + 'terms_of_service': 'true', + 'honor_code': 'true', + }) + self.assertEqual(resp.status_code, 200) + 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 + url = reverse('activate', kwargs={'key': activation_key}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + # 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 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, + }) + print ('Enrollment in %s result status code: %s' + % (course.location.url(), str(resp.status_code))) + result = resp.status_code == 200 + if verify: + self.assertTrue(result) + return result + + # 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. + 'course' is an instance of CourseDescriptor. + """ + resp = self.client.post('/change_enrollment', { + 'enrollment_action': 'unenroll', + 'course_id': course.id, + }) + self.assertEqual(resp.status_code, 200) diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py new file mode 100644 index 0000000000..81d0f4f911 --- /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_%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) diff --git a/lms/djangoapps/courseware/tests/mongo_login_helpers.py b/lms/djangoapps/courseware/tests/mongo_login_helpers.py deleted file mode 100644 index a329f71d13..0000000000 --- a/lms/djangoapps/courseware/tests/mongo_login_helpers.py +++ /dev/null @@ -1,172 +0,0 @@ -import logging -import json - -from urlparse import urlsplit, urlunsplit - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse - -from student.models import Registration - -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase - -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) - - -class MongoLoginHelpers(ModuleStoreTestCase): - - 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 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 f9ddf88b5f..4b9a5a578c 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -14,11 +14,13 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import User, Group 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 index 242379d8ca..9f9bf7ba92 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -1,378 +1,24 @@ -import logging -import json -import random - -from urlparse import urlsplit, urlunsplit -from uuid import uuid4 - -from django.contrib.auth.models import User -from django.test import TestCase -from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings -import xmodule.modulestore.django - -from student.models import Registration -from xmodule.error_module import ErrorDescriptor -from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location -from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.xml import XMLModuleStore - from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from mongo_login_helpers import * +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -log = logging.getLogger("mitx." + __name__) +import xmodule.modulestore.django - -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 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) - - -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 - - -class ActivateLoginTest(LoginEnrollmentTestCase): - '''Test logging in and logging out''' - def setUp(self): - self.setup_viewtest_user() - - def test_activate_login(self): - '''Test login -- the setup function does all the work''' - pass - - def test_logout(self): - '''Test logout -- setup function does login''' - self.logout() - - -class PageLoaderTestCase(LoginEnrollmentTestCase): - ''' 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 - courses = module_store.get_courses() - self.assertEqual(len(courses), 1) - course = courses[0] - self.enroll(course) - course_id = course.id - - # Search for items in the course - # None is treated as a wildcard - course_loc = course.location - location_query = Location(course_loc.tag, course_loc.org, - course_loc.course, None, None, None) - - items = module_store.get_items(location_query) - - if len(items) < 1: - self.fail('Could not retrieve any items from course') - else: - descriptor = random.choice(items) - - # We have ancillary course information now as modules - # and we can't simply use 'jump_to' to view them - if descriptor.location.category == 'about': - self._assert_loads('about_course', - {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'static_tab': - kwargs = {'course_id': course_id, - 'tab_slug': descriptor.location.name} - self._assert_loads('static_tab', kwargs, descriptor) - - elif descriptor.location.category == 'course_info': - self._assert_loads('info', {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'custom_tag_template': - pass - - else: - - kwargs = {'course_id': course_id, - 'location': descriptor.location.url()} - - self._assert_loads('jump_to', kwargs, descriptor, - expect_redirect=True, - check_content=True) - - 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) - - if response.status_code != 200: - self.fail('Status %d for page %s' % - (response.status_code, descriptor.location.url())) - - if expect_redirect: - self.assertEqual(response.redirect_chain[0][1], 302) - - if check_content: - unavailable_msg = "this module is temporarily unavailable" - self.assertEqual(response.content.find(unavailable_msg), -1) - self.assertFalse(isinstance(descriptor, ErrorDescriptor)) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from XML''' - - def setUp(self): - super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - - def test_toy_course_loads(self): - module_class = 'xmodule.hidden_module.HiddenDescriptor' - module_store = XMLModuleStore(TEST_DATA_DIR, - default_class=module_class, - course_dirs=['toy'], - load_error_modules=True) - - self.check_random_page_loads(module_store) +from helpers import LoginEnrollmentTestCase, check_for_get_code +from modulestore_config import TEST_DATA_MONGO_MODULESTORE @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from Mongo''' +class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): - def setUp(self): - super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - modulestore().collection.drop() + STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] - def test_toy_course_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['toy']) - self.check_random_page_loads(module_store) - - def test_full_textbooks_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['full']) - - course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) - - self.assertGreater(len(course.textbooks), 0) - - -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestNavigation(MongoLoginHelpers): - - """Check that navigation state is saved properly""" + """ + Check that navigation state is saved properly. + """ def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -388,52 +34,67 @@ class TestNavigation(MongoLoginHelpers): self.section9 = ItemFactory.create(parent_location=self.chapter9.location, display_name='factory_section') - #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) + # Create student accounts and activate them. + for i in range(len(self.STUDENT_INFO)): + self.create_account('u{0}'.format(i), self.STUDENT_INFO[i][0], self.STUDENT_INFO[i][1]) + self.activate_user(self.STUDENT_INFO[i][0]) - def test_accordion_state(self): - """Make sure that the accordion remembers where you were properly""" - self.login(self.student, self.password) - self.enroll(self.course) - self.enroll(self.full) - - # First request should redirect to ToyVideos + def test_redirects_first_time(self): + """ + Verify that the first time we click on the courseware tab we are + redirected to the 'Welcome' section. + """ + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) resp = self.client.get(reverse('courseware', kwargs={'course_id': self.course.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.course.id, 'chapter': 'Overview', 'section': 'Welcome'})) - # Hitting the couseware tab again should - # redirect to the first chapter: 'Overview' + def test_redirects_second_time(self): + """ + Verify the accordion remembers we've already visited the Welcome section + and redirects correpondingly. + """ + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, 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.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.id, - 'chapter': 'Overview'})) + self.assertRedirects(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.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.course.id, - 'chapter': 'factory_chapter', - 'section': 'factory_section'})) + def test_accordion_state(self): + """ + Verify the accordion remembers which chapter you were last viewing. + """ - # And now hitting the courseware tab should redirect to 'secret:magic' + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, 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.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.id, - 'chapter': 'factory_chapter'})) + 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 index da4f40e0db..ffae4688bf 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -1,80 +1,56 @@ -import logging import datetime import pytz import random -from uuid import uuid4 +import xmodule.modulestore.django from django.contrib.auth.models import User, Group 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.access import (has_access, _course_staff_group_name, course_beta_test_group_name) -from mongo_login_helpers import MongoLoginHelpers +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -log = logging.getLogger("mitx." + __name__) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def update_course(course, data): - """ - Updates the version of course in the mongo modulestore - with the metadata in data and returns the updated version. - """ - - store = xmodule.modulestore.django.modulestore() - - store.update_item(course.location, data) - - store.update_metadata(course.location, data) - - updated_course = store.get_instance(course.id, course.location) - - return updated_course - - -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 - - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +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(MongoLoginHelpers): - """Check that view authentication works properly""" +class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check that view authentication works properly. + """ + + ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] + + @classmethod + def _instructor_urls(self, course): + """ + List of urls that only instructors/staff should be able to see. + """ + urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( + 'instructor_dashboard', + 'gradebook', + 'grade_summary',)] + + urls.append(reverse('student_progress', + kwargs={'course_id': course.id, + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id})) + return urls + + @staticmethod + def _reverse_urls(names, course): + """ + Reverse a list of course urls. + """ + return [reverse(name, kwargs={'course_id': course.id}) + for name in names] def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -87,98 +63,105 @@ class TestViewAuth(MongoLoginHelpers): display_name='courseware') self.sub_overview_chapter = ItemFactory.create(parent_location=self.sub_courseware_chapter.location, display_name='Overview') - self.progress_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='progress') - self.info_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='info') self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, display_name='Welcome') - self.somewhere_in_progress = ItemFactory.create(parent_location=self.progress_chapter.location, - display_name='1') - # 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) + # Create two accounts and activate them. + for i in range(len(self.ACCOUNT_INFO)): + self.create_account('u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1]) + self.activate_user(self.ACCOUNT_INFO[i][0]) - 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""" + 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. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + 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. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course) - # 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.course.id})) - self.assertRedirectsNoFollow(response, - reverse('about_course', - args=[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. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course) self.enroll(self.full) - # should work now -- redirect to first page - response = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.id})) - - self.assertRedirectsNoFollow(response, - reverse('courseware_section', - kwargs={'course_id': self.course.id, - 'chapter': 'Overview', - 'section': 'Welcome'})) - - 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.course) + - instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.course) + + self._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) + check_for_get_code(self, 404, url) - # Make the instructor staff in the toy course + def test_instructor_course_access(self): + """ + Verify instructor can load the instructor dashboard, the grade views, + and student profile pages for their course. + """ + + # 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(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) - self.logout() - self.login(self.instructor, self.password) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) - # Now should be able to get to the toy course, but not the full course - url = random.choice(instructor_urls(self.course)) + # Now should be able to get to self.course, but not self.full + url = random.choice(self._instructor_urls(self.course)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 200, url) - url = random.choice(instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.full)) print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) + check_for_get_code(self, 404, url) - # now also make the instructor staff - instructor = get_user(self.instructor) + def test_instructor_as_staff_access(self): + """ + Verify the instructor can load staff pages if he is given + staff permissions. + """ + + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + + # now make the instructor also staff + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() # and now should be able to load both - url = random.choice(instructor_urls(self.course) + - instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.course) + + self._instructor_urls(self.full)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 200, url) def run_wrapped(self, test): """ @@ -196,42 +179,47 @@ class TestViewAuth(MongoLoginHelpers): 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""" + """ + 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""" + """ + Check that enrollment periods work. + """ self.run_wrapped(self._do_test_enrollment_period) def test_beta_period(self): - """Check that beta-test access works""" + """ + 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.""" + """ + Actually do the test, relying on settings to be right. + """ # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) - self.course.lms.start = tomorrow - self.full.lms.start = tomorrow + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) self.assertFalse(self.course.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 + 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 = self._reverse_urls(['info', 'progress'], course) urls.extend([ reverse('book', kwargs={'course_id': course.id, 'book_index': index}) @@ -241,38 +229,50 @@ class TestViewAuth(MongoLoginHelpers): def light_student_urls(course): """ - list of urls that students should be able to see before + List of urls that students should be able to see before launch. """ - urls = reverse_urls(['about_course'], course) + urls = self._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) + """ + List of urls that only instructors/staff should be able to see. + """ + urls = self._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""" + def check_non_staff_light(course): + """ + Check that non-staff have access to light urls. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + + # Randomly sample a light url + url = random.choice(light_student_urls(course)) + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) + + def check_non_staff_dark(course): + """ + Check that non-staff don't have access to dark urls. + """ 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)) + self._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) + check_for_get_code(self, 404, url) def check_staff(course): - """Check that access is right for staff in course""" + """ + Check that access is right for staff in course. + """ print '=== Checking staff access for {0}'.format(course.id) # Randomly sample a url @@ -280,7 +280,7 @@ class TestViewAuth(MongoLoginHelpers): dark_student_urls(course) + light_student_urls(course)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + 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 @@ -290,43 +290,46 @@ class TestViewAuth(MongoLoginHelpers): # 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}) + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) print 'checking for 404 on view-as-student: {0}'.format(url) - self.check_for_get_code(404, url) + check_for_get_code(self, 404, url) # The courseware url should redirect, not 200 - url = reverse_urls(['courseware'], course)[0] - self.check_for_get_code(302, url) + url = self._reverse_urls(['courseware'], course)[0] + check_for_get_code(self, 302, url) # First, try with an enrolled student print '=== Testing student access....' - self.login(self.student, self.password) - self.enroll(self.course) - self.enroll(self.full) + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) # shouldn't be able to get to anything except the light pages - check_non_staff(self.course) - check_non_staff(self.full) + check_non_staff_light(self.course) + check_non_staff_dark(self.course) + check_non_staff_light(self.full) + check_non_staff_dark(self.full) print '=== Testing course instructor access....' - # Make the instructor staff in the toy course + # 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(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) self.logout() - self.login(self.instructor, self.password) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) # Enroll in the classes---can't see courseware otherwise. - self.enroll(self.course) - self.enroll(self.full) + self.enroll(self.course, True) + self.enroll(self.full, True) # should now be able to get to everything for self.course - check_non_staff(self.full) + check_non_staff_light(self.full) + check_non_staff_dark(self.full) check_staff(self.course) print '=== Testing staff access....' # now also make the instructor staff - instructor = get_user(self.instructor) + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() @@ -335,7 +338,9 @@ class TestViewAuth(MongoLoginHelpers): check_staff(self.full) def _do_test_enrollment_period(self): - """Actually do the test, relying on settings to be right.""" + """ + Actually do the test, relying on settings to be right. + """ # Make courses start in the future now = datetime.datetime.now(pytz.UTC) @@ -348,42 +353,44 @@ class TestViewAuth(MongoLoginHelpers): print "changing" # self.course's enrollment period hasn't started - self.course = update_course(self.course, course_data) + self.course = self.update_course(self.course, course_data) # full course's has - self.full = update_course(self.full, full_data) + self.full = self.update_course(self.full, full_data) print "login" # First, try with an enrolled student print '=== Testing student access....' - self.login(self.student, self.password) - self.assertFalse(self.try_enroll(self.course)) - self.assertTrue(self.try_enroll(self.full)) + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.assertFalse(self.enroll(self.course)) + self.assertTrue(self.enroll(self.full)) print '=== Testing course instructor access....' - # Make the instructor staff in the toy 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(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) 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.course)) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + print "Instructor should be able to enroll in self.course" + self.assertTrue(self.enroll(self.course)) 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) + group.user_set.remove(User.objects.get(email=self.ACCOUNT_INFO[1][0])) + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() # unenroll and try again self.unenroll(self.course) - self.assertTrue(self.try_enroll(self.course)) + self.assertTrue(self.enroll(self.course)) def _do_test_beta_period(self): - """Actually test beta periods, relying on settings to be right.""" + """ + Actually test beta periods, relying on settings to be right. + """ # trust, but verify :) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) @@ -391,18 +398,17 @@ class TestViewAuth(MongoLoginHelpers): # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) - # nextday = tomorrow + 24 * 3600 - # yesterday = time.time() - 24 * 3600 + course_data = {'start': tomorrow} # self.course's hasn't started - self.course.lms.start = tomorrow + 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 = get_user(self.student) + student_user = User.objects.get(email=self.ACCOUNT_INFO[0][0]) self.assertFalse(has_access(student_user, self.course, 'load')) # now add the student to the beta test group diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 3e39227171..43b190c04b 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,275 +1,172 @@ ''' 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 -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from helpers import LoginEnrollmentTestCase +from modulestore_config import TEST_DATA_DIR, TEST_DATA_XML_MODULESTORE, TEST_DATA_MONGO_MODULESTORE, TEST_DATA_DRAFT_MONGO_MODULESTORE -log = logging.getLogger("mitx." + __name__) +class ActivateLoginTest(LoginEnrollmentTestCase): + """ + Test logging in and logging out. + """ + def setUp(self): + self.setup_user() - -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): + def test_activate_login(self): """ - 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 + Test login -- the setup function does all the work. """ - self.assertEqual(response.status_code, 302, - 'Response status code was %d instead of 302' - % (response.status_code)) - url = response['Location'] + pass - 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): + def test_logout(self): """ - Check that we got the expected code when accessing url via GET. - Returns the response. + Test logout -- setup function does login. """ - 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 + self.logout() - def check_for_post_code(self, code, url, data={}): + +class PageLoaderTestCase(LoginEnrollmentTestCase): + """ + Base class that adds a function to load all pages in a modulestore. + """ + + def check_random_page_loads(self, module_store): """ - Check that we got the expected code when accessing url via POST. - Returns the response. + Choose a page in the course randomly, and assert that it loads. """ - 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 + # 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, True) + course_id = course.id + + # Search for items in the course + # None is treated as a wildcard + course_loc = course.location + location_query = Location(course_loc.tag, course_loc.org, + course_loc.course, None, None, None) + + items = module_store.get_items(location_query) + + if len(items) < 1: + self.fail('Could not retrieve any items from course') + else: + descriptor = random.choice(items) + + # We have ancillary course information now as modules + # and we can't simply use 'jump_to' to view them + if descriptor.location.category == 'about': + self._assert_loads('about_course', + {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'static_tab': + kwargs = {'course_id': course_id, + 'tab_slug': descriptor.location.name} + self._assert_loads('static_tab', kwargs, descriptor) + + elif descriptor.location.category == 'course_info': + self._assert_loads('info', {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'custom_tag_template': + pass + + else: + + kwargs = {'course_id': course_id, + 'location': descriptor.location.url()} + + self._assert_loads('jump_to', kwargs, descriptor, + expect_redirect=True, + check_content=True) + + 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) + + if response.status_code != 200: + self.fail('Status %d for page %s' % + (response.status_code, descriptor.location.url())) + + if expect_redirect: + self.assertEqual(response.redirect_chain[0][1], 302) + + if check_content: + unavailable_msg = "this module is temporarily unavailable" + self.assertEqual(response.content.find(unavailable_msg), -1) + self.assertFalse(isinstance(descriptor, ErrorDescriptor)) + + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): + """ + Check that all pages in test courses load properly from XML. + """ + + def setUp(self): + super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() + self.setup_user() + xmodule.modulestore.django._MODULESTORES = {} + + def test_toy_course_loads(self): + module_class = 'xmodule.hidden_module.HiddenDescriptor' + module_store = XMLModuleStore(TEST_DATA_DIR, + default_class=module_class, + course_dirs=['toy'], + load_error_modules=True) + + self.check_random_page_loads(module_store) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): + """ + Check that all pages in test courses load properly from Mongo. + """ + + def setUp(self): + super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() + self.setup_user() + xmodule.modulestore.django._MODULESTORES = {} + modulestore().collection.drop() + + def test_toy_course_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['toy']) + self.check_random_page_loads(module_store) + + def test_full_textbooks_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['full']) + + course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + self.assertGreater(len(course.textbooks), 0) @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) @@ -284,134 +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 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 - - -@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 ce5f2d2e50..e70ccc6ffd 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 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 from student.models import CourseEnrollment, CourseEnrollmentAllowed @@ -40,7 +41,7 @@ class TestInstructorEnrollsStudent(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) 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 13d780df12..db19d212a2 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -8,8 +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.http import HttpResponse +from django.contrib.auth.models import Group, User from django.conf import settings from mitxmako.shortcuts import render_to_string @@ -21,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 @@ -31,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): @@ -58,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) @@ -75,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) @@ -84,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) @@ -113,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) @@ -130,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 74ac44cf59..2d85fe1e66 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -36,7 +36,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"), From 32a0a2d29dbf60713d355fc51f9a6e296af878de Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 11:13:59 -0400 Subject: [PATCH 009/440] In the middle of addressing pull request comments. This is a safety commit in case I have to revert some changes I'm about to make. --- .../xmodule/modulestore/tests/django_utils.py | 8 ++- .../xmodule/modulestore/tests/factories.py | 8 +-- .../courseware/tests/check_request_code.py | 24 ------- lms/djangoapps/courseware/tests/helpers.py | 33 ++++----- .../courseware/tests/modulestore_config.py | 16 +++-- .../courseware/tests/test_navigation.py | 16 +++-- .../tests/test_view_authentication.py | 72 +++++++++++++------ 7 files changed, 98 insertions(+), 79 deletions(-) delete mode 100644 lms/djangoapps/courseware/tests/check_request_code.py diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 944b9e5bd4..a2306a5c6b 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -14,10 +14,16 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the TestCase and drops it they are finished. """ - def update_course(self, course, data): + @staticmethod + def update_course(course, data): """ Updates the version of course in the mongo 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() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 4f63fbc1d2..82ff61204a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -60,8 +60,8 @@ 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.''' + #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 @@ -152,8 +152,8 @@ 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.''' + #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 diff --git a/lms/djangoapps/courseware/tests/check_request_code.py b/lms/djangoapps/courseware/tests/check_request_code.py deleted file mode 100644 index 1393d2fe17..0000000000 --- a/lms/djangoapps/courseware/tests/check_request_code.py +++ /dev/null @@ -1,24 +0,0 @@ - - -def check_for_get_code(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(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 diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 99da5e9061..ce0603990b 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -12,7 +12,12 @@ 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. + + `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, @@ -25,7 +30,11 @@ 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. + `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, @@ -62,8 +71,8 @@ class LoginEnrollmentTestCase(TestCase): def logout(self): """ - Logout, check that it worked. - Returns an HTTP response which e + Logout; check that the HTTP response code indicates redirection + as expected. """ resp = self.client.get(reverse('logout'), {}) # should redirect @@ -106,8 +115,8 @@ class LoginEnrollmentTestCase(TestCase): def enroll(self, course, verify=False): """ Try to enroll and return boolean indicating result. - 'course' is an instance of CourseDescriptor. - 'verify' is an optional parameter specifying whether we + `course` is an instance of CourseDescriptor. + `verify` is an optional parameter specifying whether we want to verify that the student was successfully enrolled in the course. """ @@ -122,20 +131,12 @@ class LoginEnrollmentTestCase(TestCase): self.assertTrue(result) return result - # 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. - 'course' is an instance of CourseDescriptor. + `course` is an instance of CourseDescriptor. """ - resp = self.client.post('/change_enrollment', { + resp = self.client.post(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 index 81d0f4f911..c3c4ce4e5b 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -4,11 +4,11 @@ from django.conf import settings def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore + """ + Defines default module store using MongoModuleStore. - Use of this config requires mongo to be running - ''' + Use of this config requires mongo to be running. + """ store = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', @@ -27,7 +27,9 @@ def mongo_store_config(data_dir): def draft_mongo_store_config(data_dir): - '''Defines default module store using DraftMongoModuleStore''' + """ + Defines default module store using DraftMongoModuleStore. + """ return { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', @@ -55,7 +57,9 @@ def draft_mongo_store_config(data_dir): def xml_store_config(data_dir): - '''Defines default module store using XMLModuleStore''' + """ + Defines default module store using XMLModuleStore. + """ return { 'default': { 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 9f9bf7ba92..f4662f2ef5 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -36,15 +36,18 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): # Create student accounts and activate them. for i in range(len(self.STUDENT_INFO)): - self.create_account('u{0}'.format(i), self.STUDENT_INFO[i][0], self.STUDENT_INFO[i][1]) - self.activate_user(self.STUDENT_INFO[i][0]) + 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. """ - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -61,7 +64,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): Verify the accordion remembers we've already visited the Welcome section and redirects correpondingly. """ - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -80,8 +84,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Verify the accordion remembers which chapter you were last viewing. """ - - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index ffae4688bf..5db9847d45 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -32,28 +32,40 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): @classmethod def _instructor_urls(self, course): """ - List of urls that only instructors/staff should be able to see. + `course` is an instance of CourseDescriptor whose section URLs are to be returned. + + Returns a list of URLs corresponding to sections in the passed in course. """ + urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( 'instructor_dashboard', 'gradebook', 'grade_summary',)] + email, _ = self.ACCOUNT_INFO[0] + student_id = User.objects.get(email=email).id + urls.append(reverse('student_progress', kwargs={'course_id': course.id, - 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id})) + 'student_id': student_id})) return urls @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 setUp(self): - xmodule.modulestore.django._MODULESTORES = {} self.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') self.course = CourseFactory.create() @@ -68,8 +80,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # Create two accounts and activate them. for i in range(len(self.ACCOUNT_INFO)): - self.create_account('u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1]) - self.activate_user(self.ACCOUNT_INFO[i][0]) + 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): """ @@ -77,7 +90,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instead of the 'Welcome' section after clicking on the courseware tab. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + 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, @@ -90,7 +104,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): the chapter after clicking on the courseware tab. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) self.enroll(self.course) response = self.client.get(reverse('courseware', @@ -108,7 +123,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): dashboard, the grade views, and student profile pages. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) self.enroll(self.course) self.enroll(self.full) @@ -127,12 +143,14 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=email)) - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(email, password) # Now should be able to get to self.course, but not self.full url = random.choice(self._instructor_urls(self.course)) @@ -149,10 +167,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): staff permissions. """ - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + email, password = self.ACCOUNT_INFO[1] + self.login(email, password) # now make the instructor also staff - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + instructor = User.objects.get(email=email) instructor.is_staff = True instructor.save() @@ -202,6 +221,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually do the test, relying on settings to be right. """ + 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) @@ -300,7 +322,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # First, try with an enrolled student print '=== Testing student access....' - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.login(student_email, student_password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -314,10 +336,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # 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=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=instructor_email)) self.logout() - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(instructor_email, instructor_password) # Enroll in the classes---can't see courseware otherwise. self.enroll(self.course, True) self.enroll(self.full, True) @@ -329,7 +351,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print '=== Testing staff access....' # now also make the instructor staff - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + instructor = User.objects.get(email=instructor_email) instructor.is_staff = True instructor.save() @@ -342,6 +364,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually do the test, relying on settings to be right. """ + 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) @@ -360,7 +385,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print "login" # First, try with an enrolled student print '=== Testing student access....' - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.login(student_email, student_password) self.assertFalse(self.enroll(self.course)) self.assertTrue(self.enroll(self.full)) @@ -368,18 +393,18 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # 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=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=instructor_email)) print "logout/login" self.logout() - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(instructor_email, instructor_password) print "Instructor should be able to enroll in self.course" self.assertTrue(self.enroll(self.course)) print '=== Testing staff access....' # now make the instructor global staff, but not in the instructor group - group.user_set.remove(User.objects.get(email=self.ACCOUNT_INFO[1][0])) - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + group.user_set.remove(User.objects.get(email=instructor_email)) + instructor = User.objects.get(email=instructor_email) instructor.is_staff = True instructor.save() @@ -392,6 +417,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually test beta periods, relying on settings to be right. """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + # trust, but verify :) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) @@ -408,7 +436,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.course.lms.days_early_for_beta = 2 # student user shouldn't see it - student_user = User.objects.get(email=self.ACCOUNT_INFO[0][0]) + 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 From e44ef1a54ebff3d61231ad7cc2e7ccbc5faf5933 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 16:24:09 -0400 Subject: [PATCH 010/440] Removed the use of random.choice() --- .../tests/test_view_authentication.py | 268 ++++++++++-------- 1 file changed, 153 insertions(+), 115 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 5db9847d45..8e03e2563b 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -1,8 +1,7 @@ import datetime import pytz -import random -import xmodule.modulestore.django +from mock import patch from django.contrib.auth.models import User, Group from django.conf import settings @@ -65,6 +64,99 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): return [reverse(name, kwargs={'course_id': course.id}) for name in names] + def _dark_student_urls(self, course): + """ + List of urls that students should be able to see only + after launch, but staff should see before + """ + urls = self._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(self, course): + """ + List of urls that students should be able to see before + launch. + """ + urls = self._reverse_urls(['about_course'], course) + urls.append(reverse('courses')) + + return urls + + def instructor_urls(self, course): + """ + List of urls that only instructors/staff should be able to see. + """ + urls = self._reverse_urls(['instructor_dashboard', + 'gradebook', 'grade_summary'], course) + return urls + + def _check_non_staff_light(self, course): + """ + Check that non-staff have access to light urls. + + `course` is an instance of CourseDescriptor. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')] + for url in urls: + print 'checking for 200 on {0}'.format(url) + 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. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + + 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: + print 'checking for 404 on {0}'.format(url) + check_for_get_code(self, 404, url) + + def _check_staff(self, course): + """ + Check that access is right for staff in course. + """ + print '=== Checking staff access for {0}'.format(course.id) + + 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: + print 'checking for 200 on {0}'.format(url) + 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}) + print 'checking for 404 on view-as-student: {0}'.format(url) + 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.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') @@ -129,13 +221,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.course) self.enroll(self.full) - # Randomly sample an instructor page - url = random.choice(self._instructor_urls(self.course) + - self._instructor_urls(self.full)) + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.full.id})] # Shouldn't be able to get to the instructor pages - print 'checking for 404 on {0}'.format(url) - check_for_get_code(self, 404, url) + for url in urls: + print 'checking for 404 on {0}'.format(url) + check_for_get_code(self, 404, url) def test_instructor_course_access(self): """ @@ -153,11 +245,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.login(email, password) # Now should be able to get to self.course, but not self.full - url = random.choice(self._instructor_urls(self.course)) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) - url = random.choice(self._instructor_urls(self.full)) + url = reverse('instructor_dashboard', kwargs={'course_id': self.full.id}) print 'checking for 404 on {0}'.format(url) check_for_get_code(self, 404, url) @@ -176,11 +268,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instructor.save() # and now should be able to load both - url = random.choice(self._instructor_urls(self.course) + - self._instructor_urls(self.full)) + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.full.id})] - print 'checking for 200 on {0}'.format(url) - check_for_get_code(self, 200, url) + for url in urls: + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) def run_wrapped(self, test): """ @@ -202,7 +295,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Make sure that before course start, students can't access course pages, but instructors can. """ - self.run_wrapped(self._do_test_dark_launch) + self.run_wrapped(self._do_test_dark_launch_enrolled_student) + self.run_wrapped(self._do_test_dark_launch_instructor) + self.run_wrapped(self._do_test_dark_launch_staff) def test_enrollment_period(self): """ @@ -210,19 +305,18 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ 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 test_beta_period(self): + # """ + # Check that beta-test access works. + # """ + # self.run_wrapped(self._do_test_beta_period) - def _do_test_dark_launch(self): + def _do_test_dark_launch_enrolled_student(self): """ Actually do the test, relying on settings to be right. """ 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) @@ -236,90 +330,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertFalse(self.full.has_started()) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - def dark_student_urls(course): - """ - List of urls that students should be able to see only - after launch, but staff should see before - """ - urls = self._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 = self._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 = self._reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) - return urls - - def check_non_staff_light(course): - """ - Check that non-staff have access to light urls. - """ - print '=== Checking non-staff access for {0}'.format(course.id) - - # Randomly sample a light url - url = random.choice(light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - check_for_get_code(self, 200, url) - - def check_non_staff_dark(course): - """ - Check that non-staff don't have access to dark urls. - """ - 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) + - self._reverse_urls(['courseware'], course)) - print 'checking for 404 on {0}'.format(url) - check_for_get_code(self, 404, 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) - 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}) - print 'checking for 404 on view-as-student: {0}'.format(url) - 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) - # First, try with an enrolled student print '=== Testing student access....' self.login(student_email, student_password) @@ -327,17 +337,27 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.full, True) # shouldn't be able to get to anything except the light pages - check_non_staff_light(self.course) - check_non_staff_dark(self.course) - check_non_staff_light(self.full) - check_non_staff_dark(self.full) + self._check_non_staff_light(self.course) + self._check_non_staff_dark(self.course) + self._check_non_staff_light(self.full) + self._check_non_staff_dark(self.full) + + def _do_test_dark_launch_instructor(self): + + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) print '=== Testing course instructor access....' # 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. @@ -345,9 +365,24 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.full, True) # should now be able to get to everything for self.course - check_non_staff_light(self.full) - check_non_staff_dark(self.full) - check_staff(self.course) + self._check_non_staff_light(self.full) + self._check_non_staff_dark(self.full) + self._check_staff(self.course) + + def _do_test_dark_launch_staff(self): + + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) + + self.login(instructor_email, instructor_password) + self.enroll(self.course, True) + self.enroll(self.full, True) print '=== Testing staff access....' # now also make the instructor staff @@ -356,8 +391,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instructor.save() # and now should be able to load both - check_staff(self.course) - check_staff(self.full) + self._check_staff(self.course) + self._check_staff(self.full) def _do_test_enrollment_period(self): """ @@ -412,6 +447,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.unenroll(self.course) self.assertTrue(self.enroll(self.course)) + #from courseware.access import MITX_FEATURES + + #@patch.dict(MITX_FEATURES, {'DISABLE_START_DATES': True}) def _do_test_beta_period(self): """ Actually test beta periods, relying on settings to be right. From 74bb976ef50cf0767e5f61abd056f3ea3a65b619 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 24 Jun 2013 13:32:30 -0400 Subject: [PATCH 011/440] Abort submission and alter user if gradefn throws an exception --- .../xmodule/js/src/capa/display.coffee | 31 ++++++-- common/static/js/capa/jsinput.js | 76 +++++++++++-------- doc/public/course_data_formats/jsinput.rst | 5 ++ 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 69e4551b6e..fc4c750b52 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -129,6 +129,30 @@ class @Problem if setupMethod? @inputtypeDisplays[id] = setupMethod(inputtype) + # If some function wants to be called before sending the answer to the + # server, give it a chance to do so. + # + # check_waitfor allows the callee to send alerts if the user's input is + # invalid. To do so, the callee must throw an exception named "Waitfor + # Exception". This and any other errors or exceptions that arise from the + # callee are rethrown and abort the submission. + # + # In order to use this feature, add a 'data-waitfor' attribute to the input, + # and specify the function to be called by the check button before sending + # off @answers + check_waitfor: => + for inp in @inputs + if not ($(inp).attr("data-waitfor")?) + try + $(inp).data("waitfor")() + catch e + if e.name == "Waitfor Exception" + alert e.message + else + alert "Could not grade your answer. The submission was aborted." + throw e + @refreshAnswers() + ### # 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, @@ -140,11 +164,7 @@ class @Problem check_fd: => Logger.log 'problem_check', @answers - # If some function wants to be called before sending the answer to the - # server, give it a chance to do so. - if $('input[waitfor]').length != 0 - ($(lcall).data("waitfor").call() for lcall in $('input[waitfor]')) - @refreshAnswers() + # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @check() @@ -217,6 +237,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => + @check_waitfor() Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index ff6a8aa68b..5eb1d3e360 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -10,16 +10,15 @@ // Use this array to keep track of the elements that have already been // initialized. jsinput.jsinputarr = jsinput.jsinputarr || []; - if (isFirst) { - jsinput.jsinputarr.exists = function (id) { - this.filter(function(e, i, a) { - return e.id = id; - }); - }; - } + jsinput.jsinputarr.exists = function (id) { + this.filter(function(e, i, a) { + return e.id = id; + }); + }; + function jsinputConstructor(spec) { - // Define an class that will be instantiated for each.jsinput element + // Define an class that will be instantiated for each jsinput element // of the DOM // 'that' is the object returned by the constructor. It has a single @@ -35,6 +34,11 @@ return parent.find('input[id^="input_"]'); } + // For the state and grade functions below, use functions instead of + // storing their return values since we might need to call them + // repeatedly, and they might change (e.g., they might not be defined + // when we first try calling them). + // Get the grade function name function getgradefn() { return $(sect).attr("data"); @@ -54,24 +58,24 @@ return $(sect).attr("data-stored"); } + var thisIFrame = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0); + + var cWindow = thisIFrame.contentWindow; + // Put the return value of gradefn in the hidden inputfield. // If passed an argument, does not call gradefn, and instead directly // updates the inputfield with the passed value. var update = function (answer) { var ans; - ans = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). // jquery might not be available in the iframe - contentWindow[gradefn](); + ans = cWindow[gradefn](); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getgetstate()](); + state = cWindow[getgetstate()](); store = { answer: ans, state: state @@ -91,8 +95,6 @@ $(updatebutton).click(update); } - - /* Public methods */ that.update = update; @@ -116,19 +118,30 @@ updateHandler(); bindCheck(); // Check whether application takes in state and there is a saved - // state to give it + // state to give it. If getsetstate is specified but calling it + // fails, wait and try again, since the iframe might still be + // loading. if (getsetstate() && getstoredstate()) { - console.log("Using stored state..."); var sval; if (typeof(getstoredstate()) === "object") { sval = getstoredstate()["state"]; } else { sval = getstoredstate(); } - $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getsetstate()](sval); + function whileloop(n) { + if (n < 10){ + try { + cWindow[getsetstate()](sval); + } catch (err) { + setTimeout(whileloop(n+1), 200); + } + } + else { + console.log("Error: could not set state"); + } + } + whileloop(0); + } } else { // NOT CURRENTLY SUPPORTED @@ -171,11 +184,13 @@ all.each(function() { // Get just the mako variable 'id' from the id attribute newid = $(this).attr("id").replace(/^inputtype_/, ""); - var newJsElem = jsinputConstructor({ - id: newid, - elem: this, - passive: false - }); + if (! jsinput.jsinputarr.exists(newid)){ + var newJsElem = jsinputConstructor({ + id: newid, + elem: this, + passive: false + }); + } }); } @@ -193,5 +208,6 @@ //} //}; - setTimeout(walkDOM, 200); + + setTimeout(walkDOM, 100); })(window.jsinput = window.jsinput || {}) diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 5252a5dd0c..008940e3b7 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -87,6 +87,11 @@ attributes are also used) be passed as a string to the enclosing response type. In the customresponse example above, this means cfn will be passed this answer as `ans`. +If the `gradefn` function throws an exception when a student attempts to +submit a problem, the submission is aborted, and the student receives a generic +alert. The alert can be customised by making the exception name `Waitfor +Exception`; in that case, the alert message will be the exception message. + **IMPORTANT** : the `gradefn` function should not be at all asynchronous, since this could result in the student's latest answer not being passed correctly. Moreover, the function should also return promptly, since currently the student From 986b63d85d9fda2135d239ad2caf84365b2f80d3 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:07:43 -0400 Subject: [PATCH 012/440] Removed run_wrapped() and replaced its functionality with patch.dict(): --- lms/djangoapps/courseware/tests/helpers.py | 4 ++ .../courseware/tests/test_navigation.py | 1 - .../tests/test_view_authentication.py | 64 ++++++++----------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index ce0603990b..1ceeb14433 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -44,6 +44,10 @@ def check_for_post_code(self, code, url, data={}): class LoginEnrollmentTestCase(TestCase): + """ + Provides support for user creation, + activation, login, and course enrollment. + """ def setup_user(self): """ diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index f4662f2ef5..f1aa7f5b31 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -21,7 +21,6 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): """ def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} self.course = CourseFactory.create() self.full = CourseFactory.create(display_name='Robot_Sub_Course') diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 8e03e2563b..1edeac58ed 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -10,7 +10,7 @@ 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) + course_beta_test_group_name, settings as access_settings) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -20,6 +20,7 @@ from helpers import LoginEnrollmentTestCase, check_for_get_code from modulestore_config import TEST_DATA_MONGO_MODULESTORE +#@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': True}) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -81,20 +82,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ List of urls that students should be able to see before launch. + `course` is an instance of CourseDescriptor. """ urls = self._reverse_urls(['about_course'], course) urls.append(reverse('courses')) return urls - def instructor_urls(self, course): - """ - List of urls that only instructors/staff should be able to see. - """ - urls = self._reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) - return urls - def _check_non_staff_light(self, course): """ Check that non-staff have access to light urls. @@ -295,25 +289,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Make sure that before course start, students can't access course pages, but instructors can. """ - self.run_wrapped(self._do_test_dark_launch_enrolled_student) - self.run_wrapped(self._do_test_dark_launch_instructor) - self.run_wrapped(self._do_test_dark_launch_staff) - def test_enrollment_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_enrolled_student(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_enrolled_student(self): - """ - Actually do the test, relying on settings to be right. + Make sure that before course start, students can't access course + pages. """ student_email, student_password = self.ACCOUNT_INFO[0] @@ -328,7 +309,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertFalse(self.course.has_started()) self.assertFalse(self.full.has_started()) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) # First, try with an enrolled student print '=== Testing student access....' @@ -342,7 +322,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_non_staff_light(self.full) self._check_non_staff_dark(self.full) - def _do_test_dark_launch_instructor(self): + @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] @@ -369,7 +354,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_non_staff_dark(self.full) self._check_staff(self.course) - def _do_test_dark_launch_staff(self): + @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] @@ -394,9 +384,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_staff(self.course) self._check_staff(self.full) - def _do_test_enrollment_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_enrollment_period(self): """ - Actually do the test, relying on settings to be right. + Check that enrollment periods work. """ student_email, student_password = self.ACCOUNT_INFO[0] @@ -447,20 +438,15 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.unenroll(self.course) self.assertTrue(self.enroll(self.course)) - #from courseware.access import MITX_FEATURES - - #@patch.dict(MITX_FEATURES, {'DISABLE_START_DATES': True}) - def _do_test_beta_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_beta_period(self): """ - Actually test beta periods, relying on settings to be right. + Check that beta-test access works. """ student_email, student_password = self.ACCOUNT_INFO[0] instructor_email, instructor_password = self.ACCOUNT_INFO[1] - # trust, but verify :) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) From 7fd1190505fde49e24763927721701187bcceaf0 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:13:33 -0400 Subject: [PATCH 013/440] Updated a doc string. --- lms/djangoapps/courseware/tests/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 1ceeb14433..a02a0dfe50 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -120,7 +120,7 @@ class LoginEnrollmentTestCase(TestCase): """ Try to enroll and return boolean indicating result. `course` is an instance of CourseDescriptor. - `verify` is an optional parameter specifying whether we + `verify` is an optional boolean parameter specifying whether we want to verify that the student was successfully enrolled in the course. """ From 1b344e4d4d14e7996ffff393a0d6d8d50f6aeae5 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:20:59 -0400 Subject: [PATCH 014/440] Removed some unused functions. --- .../tests/test_view_authentication.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 1edeac58ed..6a6c539b60 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -29,27 +29,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] - @classmethod - def _instructor_urls(self, course): - """ - `course` is an instance of CourseDescriptor whose section URLs are to be returned. - - Returns a list of URLs corresponding to sections in the passed in course. - """ - - urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( - 'instructor_dashboard', - 'gradebook', - 'grade_summary',)] - - email, _ = self.ACCOUNT_INFO[0] - student_id = User.objects.get(email=email).id - - urls.append(reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': student_id})) - return urls - @staticmethod def _reverse_urls(names, course): """ @@ -65,30 +44,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): return [reverse(name, kwargs={'course_id': course.id}) for name in names] - def _dark_student_urls(self, course): - """ - List of urls that students should be able to see only - after launch, but staff should see before - """ - urls = self._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(self, course): - """ - List of urls that students should be able to see before - launch. - `course` is an instance of CourseDescriptor. - """ - urls = self._reverse_urls(['about_course'], course) - urls.append(reverse('courses')) - - return urls - def _check_non_staff_light(self, course): """ Check that non-staff have access to light urls. From c4c68f516b796c8f9ac837ff30fab9ac59126771 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:31:49 -0400 Subject: [PATCH 015/440] Removed some unnecessary imports. --- .../courseware/tests/test_navigation.py | 2 -- .../courseware/tests/test_view_authentication.py | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index f1aa7f5b31..eaeb062504 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -5,8 +5,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -import xmodule.modulestore.django - from helpers import LoginEnrollmentTestCase, check_for_get_code from modulestore_config import TEST_DATA_MONGO_MODULESTORE diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 6a6c539b60..b118f99ca2 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -224,21 +224,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 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 From 1e0702f374e7bd76ee24dc334a0894ceac6b76eb Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Tue, 25 Jun 2013 16:14:34 -0400 Subject: [PATCH 016/440] Allow nested object methods for the grade and state functions --- common/static/js/capa/jsinput.js | 29 +++++++++++++++++++--- doc/public/course_data_formats/jsinput.rst | 13 ++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index 5eb1d3e360..6b7cac2429 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -4,6 +4,29 @@ // most relevantly, jquery). Keep in mind what happens in which context // when modifying this file. + + // _deepKey and _ctxCall are helper functions used to ensure that gradefn + // etc. can be nested objects (e.g., "firepad.getText") and that when + // called they receive the appropriate objects as "this" (e.g., "firepad"). + + // Take a string and find the nested object that corresponds to it. E.g.: + // deepKey(obj, "an.example") -> obj["an"]["example"] + var _deepKey = function(obj, path){ + for (var i = 0, path=path.split('.'), len = path.length; i < len; i++){ + obj = obj[path[i]]; + } + return obj; + }; + + var _ctxCall = function(obj, fn) { + var func = _deepKey(obj, fn); + var oldthis = fn.split('.'); + oldthis.pop(); + oldthis = oldthis.join(); + var newthis = _deepKey(obj, oldthis); + return func.apply(newthis); + }; + // First time this function was called? var isFirst = typeof(jsinput.jsinputarr) != 'undefined'; @@ -70,12 +93,12 @@ var update = function (answer) { var ans; - ans = cWindow[gradefn](); + ans = _ctxCall(cWindow, gradefn); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = cWindow[getgetstate()](); + state = _ctxCall(cWindow, getgetstate()); store = { answer: ans, state: state @@ -131,7 +154,7 @@ function whileloop(n) { if (n < 10){ try { - cWindow[getsetstate()](sval); + _ctxCall(cWindow, getsetstate(), sval); } catch (err) { setTimeout(whileloop(n+1), 200); } diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 008940e3b7..5cf043a3ce 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -66,11 +66,14 @@ should be located in the content directory. The iframe is created using the sandbox attribute; while popups, scripts, and pointer locks are allowed, the iframe cannot access its parent's attributes. -The html file should contain a top-level function for the gradefn function. To -check whether the gradefn will be accessible to JSInput, check that, in the -console,:: - window["`gradefn`"] -Returns the right thing. +The html file should contain an accesible gradefn function. To check whether +the gradefn will be accessible to JSInput, check that, in the console,:: + "`gradefn" +Returns the right thing. When used by JSInput, `gradefn` is called with:: + `gradefn`.call(`obj`) +Where `obj` is the object-part of `gradefn`. For example, if `gradefn` is +`myprog.myfn`, JSInput will call `myprog.myfun.call(myprog)`. (This is to +ensure "`this`" continues to refer to what `gradefn` expects.) Aside from that, more or less anything goes. Note that currently there is no support for inheriting css or javascript from the parent (aside from the From 46788f31c8b198d5a73542419ad6657edb1969b8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 26 Jun 2013 10:12:40 -0400 Subject: [PATCH 017/440] Remove obsolete and distracting SCSS file --- .../sass/course/_discussions-inline.scss | 535 ------------------ 1 file changed, 535 deletions(-) delete mode 100644 lms/static/sass/course/_discussions-inline.scss diff --git a/lms/static/sass/course/_discussions-inline.scss b/lms/static/sass/course/_discussions-inline.scss deleted file mode 100644 index f9569a80ff..0000000000 --- a/lms/static/sass/course/_discussions-inline.scss +++ /dev/null @@ -1,535 +0,0 @@ -.discussion-module { - @extend .discussion-body; - margin: 20px 0; - padding: 20px 20px 28px 20px; - background: #f6f6f6 !important; - border-radius: 3px; - - .responses { - margin-top: 40px; - - > li { - margin: 0 20px 30px; - } - } - - .discussion-show { - display: block; - width: 200px; - margin: auto; - font-size: 14px; - text-align: center; - - &.shown { - .show-hide-discussion-icon { - background-position: 0 0; - } - } - - .show-hide-discussion-icon { - display: inline-block; - position: relative; - top: 5px; - margin-right: 6px; - width: 21px; - height: 19px; - background: url(../images/show-hide-discussion-icon.png) no-repeat; - background-position: -21px 0; - } - } - - .new-post-btn { - display: inline-block; - } - - section.discussion { - margin-top: 20px; - - .threads { - margin-top: 20px; - } - - /* Course content p has a default margin-bottom of 1.416em, this is just to reset that */ - .discussion-thread { - padding: 0; - @include transition(all .25s); - - .dogear, - .vote-btn { - display: none; - } - - &.expanded { - padding: 20px 0; - - .dogear, - .vote-btn { - display: block; - } - - .discussion-article { - border: 1px solid #b2b2b2; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); - border-radius: 3px; - } - } - - p { - margin-bottom: 0em; - } - - .discussion-article { - border: 1px solid #ddd; - border-bottom-width: 0; - background: #fff; - min-height: 0; - padding: 10px 10px 15px 10px; - box-shadow: 0 1px 0 #ddd; - @include transition(all .2s); - - .discussion-post { - padding: 12px 20px 0 20px; - @include clearfix; - - header { - padding-bottom: 0; - margin-bottom: 15px; - - h3 { - font-size: 19px; - font-weight: 700; - margin-bottom: 0px; - } - - h4 { - font-size: 16px; - } - } - - .post-body { - font-size: 14px; - clear: both; - } - } - - .post-tools { - margin-left: 20px; - - a { - display: block; - font-size: 12px; - line-height: 30px; - - &.expand-post:before { - content: 'â–¾ '; - } - - &.collapse-post:before { - content: 'â–´ '; - } - - &.collapse-post { - display: none; - } - } - } - - .responses { - margin-top: 10px; - - header { - padding-bottom: 0em; - margin-bottom: 5px; - - .posted-by { - font-size: 0.8em; - } - } - .response-body { - margin-bottom: 0.2em; - font-size: 14px; - } - } - - .discussion-reply-new { - .wmd-input { - height: 120px; - } - } - - // Content that is hidden by default in the inline view - .post-extended-content{ - display: none; - } - - - } - } - } - - .new-post-article { - display: none; - margin-top: 20px; - - .inner-wrapper { - max-width: 1180px; - min-width: 760px; - margin: auto; - } - - .new-post-form { - width: 100%; - margin-bottom: 20px; - padding: 30px; - border-radius: 3px; - background: rgba(0, 0, 0, .55); - color: #fff; - box-shadow: none; - @include clearfix; - @include box-sizing(border-box); - - .form-row { - margin-bottom: 20px; - } - - .new-post-body .wmd-input { - @include discussion-wmd-input; - position: relative; - width: 100%; - height: 200px; - z-index: 1; - padding: 10px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 3px 3px 0 0; - background: #fff; - font-family: 'Monaco', monospace; - font-size: 13px; - line-height: 1.6; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-body .wmd-preview { - @include discussion-wmd-preview; - position: relative; - width: 100%; - //height: 50px; - margin-top: -1px; - padding: 25px 20px 10px 20px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 0 0 3px 3px; - background: #e6e6e6; - color: #333; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-preview-label { - position: absolute; - top: 4px; - left: 4px; - font-size: 11px; - color: #aaa; - text-transform: uppercase; - } - - .new-post-title, - .new-post-tags { - width: 100%; - height: 40px; - padding: 0 10px; - box-sizing: border-box; - border-radius: 3px; - border: 1px solid #333; - font-size: 16px; - font-family: 'Open Sans', sans-serif; - color: #333; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - } - - .new-post-title { - font-weight: 700; - } - - .tagsinput { - padding: 10px; - box-sizing: border-box; - border: 1px solid #333; - border-radius: 3px; - background: #fff; - font-family: 'Monaco', monospace; - font-size: 13px; - line-height: 1.6; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset; - - span.tag { - margin-bottom: 0; - } - } - - .submit { - @include blue-button; - float: left; - height: 37px; - margin-top: 10px; - padding-bottom: 2px; - border-color: #333; - - &:hover { - border-color: #222; - } - } - - .new-post-cancel { - @include white-button; - float: left; - margin: 10px 0 0 15px; - border-color: #444; - } - - .options { - margin-top: 5px; - - label { - display: inline; - margin-left: 8px; - font-size: 15px; - color: #fff; - text-shadow: none; - } - } - } - - .thread-tags { - margin-top: 20px; - } - - .thread-tag { - padding: 3px 10px 6px; - border-radius: 3px; - color: #333; - background: #c5eeff; - border: 1px solid #90c4d7; - font-size: 13px; - } - - .thread-title { - display: block; - margin-bottom: 20px; - font-size: 21px; - color: #333; - font-weight: 700; - } - } - - .new-post-btn { - @include blue-button; - display: inline-block; - font-size: 13px; - margin-right: 4px; - } - - .new-post-icon { - display: block; - float: left; - width: 16px; - height: 17px; - margin: 8px 7px 0 0; - background: url(../images/new-post-icon.png) no-repeat; - } - - .moderator-actions { - padding-left: 0 !important; - } - - section.pagination { - margin-top: 30px; - - nav.discussion-paginator { - float: right; - - ol { - li { - list-style: none; - display: inline-block; - padding-right: 0.5em; - a { - @include white-button; - } - } - - li.current-page{ - height: 35px; - padding: 0 15px; - border: 1px solid #ccc; - border-radius: 3px; - font-size: 13px; - font-weight: 700; - line-height: 32px; - color: #333; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - } - } - } - } - - .new-post-body { - .wmd-panel { - width: 100%; - min-width: 500px; - } - - .wmd-button-bar { - width: 100%; - } - - .wmd-input { - height: 150px; - width: 100%; - background-color: #e9e9e9; - border: 1px solid #c8c8c8; - font-family: Monaco, 'Lucida Console', monospace; - font-style: normal; - font-size: 0.8em; - line-height: 1.6em; - @include border-radius(3px 3px 0 0); - - &::-webkit-input-placeholder { - color: #888; - } - } - - .wmd-preview { - position: relative; - font-family: $sans-serif; - padding: 25px 20px 10px 20px; - margin-bottom: 5px; - box-sizing: border-box; - border: 1px solid #c8c8c8; - border-top-width: 0; - @include border-radius(0 0 3px 3px); - overflow: hidden; - @include transition(all, .2s, easeOut); - - &:before { - content: 'PREVIEW'; - position: absolute; - top: 3px; - left: 5px; - font-size: 11px; - color: #bbb; - } - - p { - font-family: $sans-serif; - } - background-color: #fafafa; - } - - .wmd-button-row { - position: relative; - margin-left: 5px; - margin-right: 5px; - margin-bottom: 5px; - margin-top: 10px; - padding: 0px; - height: 20px; - overflow: hidden; - @include transition(all, .2s, easeOut); - } - - .wmd-spacer { - width: 1px; - height: 20px; - margin-left: 14px; - - position: absolute; - background-color: Silver; - display: inline-block; - list-style: none; - } - - .wmd-button { - width: 20px; - height: 20px; - padding-left: 2px; - padding-right: 3px; - position: absolute; - display: inline-block; - list-style: none; - cursor: pointer; - background: none; - } - - .wmd-button > span { - display: inline-block; - background-image: url(../images/new-post-icons-full.png); - background-repeat: no-repeat; - background-position: 0px 0px; - width: 20px; - height: 20px; - } - - .wmd-spacer1 { - left: 50px; - } - .wmd-spacer2 { - left: 175px; - } - - .wmd-spacer3 { - left: 300px; - } - - .wmd-prompt-background { - background-color: Black; - } - - .wmd-prompt-dialog { - @extend .modal; - background: #fff; - } - - .wmd-prompt-dialog { - padding: 20px; - - > div { - font-size: 0.8em; - font-family: arial, helvetica, sans-serif; - } - - b { - font-size: 16px; - } - - > form > input[type="text"] { - border-radius: 3px; - color: #333; - } - - > form > input[type="button"] { - border: 1px solid #888; - font-family: $sans-serif; - font-size: 14px; - } - - > form > input[type="file"] { - margin-bottom: 18px; - } - } - } - - .wmd-button-row { - // this is being hidden now because the inline styles to position the icons are not being written - display: none; - position: relative; - height: 12px; - } - - .wmd-button { - span { - background-image: url("/static/images/wmd-buttons.png"); - display: inline-block; - } - } -} \ No newline at end of file From 9d49a0e8b973f28527b945842318847d33d4a360 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 26 Jun 2013 12:35:34 -0400 Subject: [PATCH 018/440] Fix check for waitfor attr --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index fc4c750b52..d1091e6005 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -142,7 +142,7 @@ class @Problem # off @answers check_waitfor: => for inp in @inputs - if not ($(inp).attr("data-waitfor")?) + if ($(inp).attr("waitfor")?) try $(inp).data("waitfor")() catch e From b03d93901f9f1cc97bb3d20586436d576fe75466 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 26 Jun 2013 12:36:04 -0400 Subject: [PATCH 019/440] Pass arguments from ctxCall forward to set state --- common/static/js/capa/jsinput.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index 6b7cac2429..0137d4ca7b 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -24,7 +24,11 @@ oldthis.pop(); oldthis = oldthis.join(); var newthis = _deepKey(obj, oldthis); - return func.apply(newthis); + + var args = Array.prototype.slice.call(arguments); + args = args.slice(2, args.length); + + return func.apply(newthis, args); }; // First time this function was called? From 621777e6ae877a2a4914ad8492fdc969935b20ec Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 26 Jun 2013 16:01:31 -0400 Subject: [PATCH 020/440] Explicitly test the creation and deletion of every component type The testing of all settings/text editing have been done elsewhere and should be done when testing a new component --- .../contentstore/features/component.feature | 71 ++++++++++ .../contentstore/features/component.py | 126 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 cms/djangoapps/contentstore/features/component.feature create mode 100644 cms/djangoapps/contentstore/features/component.py diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature new file mode 100644 index 0000000000..b3d446fc3b --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.feature @@ -0,0 +1,71 @@ +Feature: Component Adding + As a course author, I want to be able to add a wide variety of components + + Scenario: I can add components + Given I have opened a new course in studio + And I am on a new unit + When I add the following components: + | Component | + | Discussion | + | Announcement | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + Then I see the following components: + | Component | + | Discussion | + | Announcement | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + + + Scenario: I can delete Components + Given I have opened a new course in studio + And I am on a new unit + And I add the following components: + | Component | + | Discussion | + | Announcement | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + When I will confirm all alerts + And I delete all components + Then I see no components diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py new file mode 100644 index 0000000000..7366adf07c --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.py @@ -0,0 +1,126 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step + +data_location = 'i4x://edx/templates' + + +@step(u'I am on a new unit') +def add_unit(step): + section_css = 'a.new-courseware-section-button' + world.css_click(section_css) + save_section_css = 'input.new-section-name-save' + world.css_click(save_section_css) + subsection_css = 'a.new-subsection-item' + world.css_click(subsection_css) + save_subsection_css = 'input.new-subsection-name-save' + world.css_click(save_subsection_css) + expand_css = 'div.section-item a.expand-collapse-icon' + world.css_click(expand_css) + unit_css = 'a.new-unit-item' + world.css_click(unit_css) + + +@step(u'I add the following components:') +def add_components(step): + for component in step.hashes: + #due to the way lettuce stores the dictionary + component = component['Component'] + #from pdb import set_trace; set_trace() + assert component in component_dictionary + how_to_add = component_dictionary[component]['steps'] + for css in how_to_add: + world.css_click(css) + + +@step(u'I see the following components') +def check_components(step): + for component in step.hashes: + component = component['Component'] + assert component in component_dictionary + assert component_dictionary[component]['found']() + + +@step(u'I delete all components') +def delete_all_components(step): + components_num = len(component_dictionary) + for delete in range(0, components_num): + world.css_click('a.delete-button') + + +@step(u'I see no components') +def see_no_components(steps): + assert world.is_css_not_present('li.component') + + +component_dictionary = { + 'Discussion': { + 'steps': ['a[data-type="discussion"]'], + 'found': lambda: world.is_css_present('section.xmodule_DiscussionModule', wait_time=2) + }, + 'Announcement': { + 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/Announcement"]' % data_location], + 'found': lambda: world.browser.is_text_present('Heading of document') + }, + 'Blank HTML': { + 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/Blank_HTML_Page"]' % data_location], + 'found': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')] + }, + 'LaTex': { + 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/E-text_Written_in_LaTeX"]' % data_location], + 'found': lambda: world.browser.is_text_present('EXAMPLE: E-TEXT PAGE', wait_time=2) + }, + 'Blank Problem': { + 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Blank_Common_Problem"]' % data_location], + 'found': lambda: world.browser.is_text_present('BLANK COMMON PROBLEM', wait_time=2) + }, + 'Dropdown': { + 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Dropdown"]' % data_location], + 'found': lambda: world.browser.is_text_present('DROPDOWN', wait_time=2) + }, + 'Multi Choice': { + 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Multiple_Choice"]' % data_location], + 'found': lambda: world.browser.is_text_present('MULTIPLE CHOICE', wait_time=2) + }, + 'Numerical': { + 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Numerical_Input"]' % data_location], + 'found': lambda: world.browser.is_text_present('NUMERICAL INPUT', wait_time=2) + }, + 'Text Input': { + 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Text_Input"]' % data_location], + 'found': lambda: world.browser.is_text_present('TEXT INPUT', wait_time=2) + }, + 'Advanced': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Blank_Advanced_Problem"]' % data_location], + 'found': lambda: world.browser.is_text_present('BLANK ADVANCED PROBLEM', wait_time=2) + }, + 'Circuit': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Circuit_Schematic_Builder"]' % data_location], + 'found': lambda: world.browser.is_text_present('CIRCUIT SCHEMATIC BUILDER', wait_time=2) + }, + 'Custom Python': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Custom_Python-Evaluated_Input"]' % data_location], + 'found': lambda: world.browser.is_text_present('CUSTOM PYTHON-EVALUATED INPUT', wait_time=2) + }, + 'Image Mapped': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Image_Mapped_Input"]' % data_location], + 'found': lambda: world.browser.is_text_present('IMAGE MAPPED INPUT', wait_time=2) + }, + 'Math Input': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Math_Expression_Input"]' % data_location], + 'found': lambda: world.browser.is_text_present('MATH EXPRESSION INPUT', wait_time=2) + }, + 'Problem LaTex': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Problem_Written_in_LaTeX"]' % data_location], + 'found': lambda: world.browser.is_text_present('PROBLEM WRITTEN IN LATEX', wait_time=2) + }, + 'Adaptive Hint': { + 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Problem_with_Adaptive_Hint"]' % data_location], + 'found': lambda: world.browser.is_text_present('PROBLEM WITH ADAPTIVE HINT', wait_time=2) + }, + 'Video': { + 'steps': ['a[data-type="video"]'], + 'found': lambda: world.is_css_present('section.xmodule_VideoModule', wait_time=2) + } +} From 7f017d0ca98c8b1c1b40b380c0e97186e9bbf9bf Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Wed, 26 Jun 2013 14:50:16 -0400 Subject: [PATCH 021/440] Addressed pull request feedback --- common/djangoapps/student/views.py | 2 +- .../xmodule/modulestore/tests/django_utils.py | 46 ++++----- .../xmodule/modulestore/tests/factories.py | 8 +- lms/djangoapps/courseware/access.py | 1 - lms/djangoapps/courseware/tests/helpers.py | 27 ++---- .../courseware/tests/modulestore_config.py | 30 +++--- .../courseware/tests/test_navigation.py | 10 +- .../tests/test_view_authentication.py | 94 ++++++------------- 8 files changed, 78 insertions(+), 140 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 82c17c0e67..6b9c9104c5 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -357,7 +357,6 @@ def change_enrollment(request): return HttpResponseBadRequest("Course id not specified") if action == "enroll": - # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from try: @@ -366,6 +365,7 @@ def change_enrollment(request): log.warning("User {0} tried to enroll in non-existent course {1}" .format(user.username, course_id)) return HttpResponseBadRequest("Course id is invalid") + if not has_access(user, course, 'enroll'): return HttpResponseBadRequest("Enrollment is closed") diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index a2306a5c6b..27f0d9cf2a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -17,30 +17,24 @@ class ModuleStoreTestCase(TestCase): @staticmethod def update_course(course, data): """ - Updates the version of course in the mongo modulestore - with the metadata in data and returns the updated version. + 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_item(course.location, data) - 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 @@ -52,11 +46,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 @@ -68,9 +62,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. @@ -88,9 +82,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() @@ -102,9 +96,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() @@ -116,9 +110,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 82ff61204a..a7f0a71a59 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -60,8 +60,8 @@ 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. + # 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 @@ -152,8 +152,8 @@ 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. + # 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 diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 8150d380c5..762020c421 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -526,7 +526,6 @@ def _adjust_start_date_for_beta_testers(user, descriptor): start_as_datetime = descriptor.lms.start delta = timedelta(descriptor.lms.days_early_for_beta) effective = start_as_datetime - delta - # ...and back to time_struct return effective diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index a02a0dfe50..6890a6df2a 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -48,7 +48,6 @@ 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. @@ -67,8 +66,8 @@ class LoginEnrollmentTestCase(TestCase): """ Login, check that the corresponding view's response has a 200 status code. """ - resp = resp = self.client.post(reverse('login'), - {'email': email, 'password': password}) + 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']) @@ -78,15 +77,14 @@ class LoginEnrollmentTestCase(TestCase): Logout; check that the HTTP response code indicates redirection as expected. """ - resp = self.client.get(reverse('logout'), {}) # should redirect - self.assertEqual(resp.status_code, 302) + check_for_get_code(self, 302, reverse('logout')) def create_account(self, username, email, password): """ Create the account and check that it worked. """ - resp = self.client.post(reverse('create_account'), { + resp = check_for_post_code(self, 200, reverse('create_account'), { 'username': username, 'email': email, 'password': password, @@ -94,10 +92,8 @@ class LoginEnrollmentTestCase(TestCase): 'terms_of_service': 'true', 'honor_code': 'true', }) - self.assertEqual(resp.status_code, 200) 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) @@ -107,12 +103,8 @@ class LoginEnrollmentTestCase(TestCase): No error checking. """ activation_key = Registration.objects.get(user__email=email).activation_key - # and now we try to activate - url = reverse('activate', kwargs={'key': activation_key}) - - resp = self.client.get(url) - self.assertEqual(resp.status_code, 200) + 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) @@ -128,8 +120,6 @@ class LoginEnrollmentTestCase(TestCase): 'enrollment_action': 'enroll', 'course_id': course.id, }) - print ('Enrollment in %s result status code: %s' - % (course.location.url(), str(resp.status_code))) result = resp.status_code == 200 if verify: self.assertTrue(result) @@ -140,8 +130,5 @@ class LoginEnrollmentTestCase(TestCase): Unenroll the currently logged-in user, and check that it worked. `course` is an instance of CourseDescriptor. """ - resp = self.client.post(reverse('change_enrollment'), { - 'enrollment_action': 'unenroll', - 'course_id': course.id, - }) - self.assertEqual(resp.status_code, 200) + 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 index c3c4ce4e5b..9515e449f9 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -16,7 +16,7 @@ def mongo_store_config(data_dir): 'default_class': 'xmodule.raw_module.RawDescriptor', 'host': 'localhost', 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, + 'collection': 'modulestore', 'fs_root': data_dir, 'render_template': 'mitxmako.shortcuts.render_to_string' } @@ -30,28 +30,24 @@ 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': { - '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', - } + 'OPTIONS': modulestore_options }, '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', - } + 'OPTIONS': modulestore_options } } diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index eaeb062504..282f6383fc 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -20,8 +20,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): def setUp(self): - self.course = CourseFactory.create() - self.full = CourseFactory.create(display_name='Robot_Sub_Course') + 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, @@ -46,7 +46,7 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): email, password = self.STUDENT_INFO[0] self.login(email, password) self.enroll(self.course, True) - self.enroll(self.full, True) + self.enroll(self.test_course, True) resp = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) @@ -64,7 +64,7 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): email, password = self.STUDENT_INFO[0] self.login(email, password) self.enroll(self.course, True) - self.enroll(self.full, True) + self.enroll(self.test_course, True) self.client.get(reverse('courseware_section', kwargs={'course_id': self.course.id, 'chapter': 'Overview', @@ -84,7 +84,7 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): email, password = self.STUDENT_INFO[0] self.login(email, password) self.enroll(self.course, True) - self.enroll(self.full, 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', diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index b118f99ca2..055c860fcc 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -4,7 +4,6 @@ import pytz from mock import patch from django.contrib.auth.models import User, Group -from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings @@ -20,7 +19,6 @@ from helpers import LoginEnrollmentTestCase, check_for_get_code from modulestore_config import TEST_DATA_MONGO_MODULESTORE -#@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': True}) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -50,17 +48,14 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): `course` is an instance of CourseDescriptor. """ - print '=== Checking non-staff access for {0}'.format(course.id) urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')] for url in urls: - print 'checking for 200 on {0}'.format(url) 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. """ - print '=== Checking non-staff access for {0}'.format(course.id) names = ['courseware', 'instructor_dashboard', 'progress'] urls = self._reverse_urls(names, course) @@ -70,15 +65,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): for index, book in enumerate(course.textbooks) ]) for url in urls: - print 'checking for 404 on {0}'.format(url) check_for_get_code(self, 404, url) def _check_staff(self, course): """ Check that access is right for staff in course. """ - print '=== Checking staff access for {0}'.format(course.id) - names = ['about_course', 'instructor_dashboard', 'progress'] urls = self._reverse_urls(names, course) urls.extend([ @@ -87,7 +79,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): for index, book in enumerate(course.textbooks) ]) for url in urls: - print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) # The student progress tab is not accessible to a student @@ -99,7 +90,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): url = reverse('student_progress', kwargs={'course_id': course.id, 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) - print 'checking for 404 on view-as-student: {0}'.format(url) check_for_get_code(self, 404, url) # The courseware url should redirect, not 200 @@ -108,11 +98,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): def setUp(self): - self.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') - self.course = CourseFactory.create() + 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.sub_courseware_chapter = ItemFactory.create(parent_location=self.full.location, + + 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') @@ -130,7 +121,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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', @@ -144,7 +134,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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) @@ -163,19 +152,17 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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.full) + self.enroll(self.test_course) urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), - reverse('instructor_dashboard', kwargs={'course_id': self.full.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: - print 'checking for 404 on {0}'.format(url) check_for_get_code(self, 404, url) def test_instructor_course_access(self): @@ -183,7 +170,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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 @@ -193,13 +179,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.login(email, password) - # Now should be able to get to self.course, but not self.full + # Now should be able to get to self.course, but not self.test_course url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) - print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) - url = reverse('instructor_dashboard', kwargs={'course_id': self.full.id}) - print 'checking for 404 on {0}'.format(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): @@ -207,7 +191,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Verify the instructor can load staff pages if he is given staff permissions. """ - email, password = self.ACCOUNT_INFO[1] self.login(email, password) @@ -218,18 +201,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # 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.full.id})] + reverse('instructor_dashboard', kwargs={'course_id': self.test_course.id})] for url in urls: - print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) - def test_dark_launch(self): - """ - Make sure that before course start, students can't access course - pages, but instructors can. - """ - @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) def test_dark_launch_enrolled_student(self): """ @@ -243,24 +219,23 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) course_data = {'start': tomorrow} - full_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} self.course = self.update_course(self.course, course_data) - self.full = self.update_course(self.full, full_data) + self.test_course = self.update_course(self.test_course, test_course_data) self.assertFalse(self.course.has_started()) - self.assertFalse(self.full.has_started()) + self.assertFalse(self.test_course.has_started()) # First, try with an enrolled student - print '=== Testing student access....' self.login(student_email, student_password) self.enroll(self.course, True) - self.enroll(self.full, 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.full) - self._check_non_staff_dark(self.full) + 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): @@ -268,17 +243,15 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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} - full_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} self.course = self.update_course(self.course, course_data) - self.full = self.update_course(self.full, full_data) + self.test_course = self.update_course(self.test_course, test_course_data) - print '=== Testing course instructor access....' # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) @@ -287,11 +260,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.login(instructor_email, instructor_password) # Enroll in the classes---can't see courseware otherwise. self.enroll(self.course, True) - self.enroll(self.full, True) + self.enroll(self.test_course, True) # should now be able to get to everything for self.course - self._check_non_staff_light(self.full) - self._check_non_staff_dark(self.full) + 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}) @@ -300,21 +273,19 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): 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} - full_data = {'start': tomorrow} + test_course_data = {'start': tomorrow} self.course = self.update_course(self.course, course_data) - self.full = self.update_course(self.full, full_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.full, True) + self.enroll(self.test_course, True) - print '=== Testing staff access....' # now also make the instructor staff instructor = User.objects.get(email=instructor_email) instructor.is_staff = True @@ -322,14 +293,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # and now should be able to load both self._check_staff(self.course) - self._check_staff(self.full) + 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] @@ -340,34 +310,27 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): yesterday = now - datetime.timedelta(days=1) course_data = {'enrollment_start': tomorrow, 'enrollment_end': nextday} - full_data = {'enrollment_start': yesterday, 'enrollment_end': tomorrow} + test_course_data = {'enrollment_start': yesterday, 'enrollment_end': tomorrow} - print "changing" # self.course's enrollment period hasn't started self.course = self.update_course(self.course, course_data) - # full course's has - self.full = self.update_course(self.full, full_data) + # test_course course's has + self.test_course = self.update_course(self.test_course, test_course_data) - print "login" # First, try with an enrolled student - print '=== Testing student access....' self.login(student_email, student_password) self.assertFalse(self.enroll(self.course)) - self.assertTrue(self.enroll(self.full)) + self.assertTrue(self.enroll(self.test_course)) - print '=== Testing course instructor access....' # 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)) - print "logout/login" self.logout() self.login(instructor_email, instructor_password) - print "Instructor should be able to enroll in self.course" self.assertTrue(self.enroll(self.course)) - print '=== Testing staff access....' # 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) @@ -383,7 +346,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Check that beta-test access works. """ - student_email, student_password = self.ACCOUNT_INFO[0] instructor_email, instructor_password = self.ACCOUNT_INFO[1] From 42f71156debfcb0735541ace593f8eb5c919d249 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 26 Jun 2013 14:42:17 -0400 Subject: [PATCH 022/440] Script for making all instructors content creators. --- cms/djangoapps/auth/authz.py | 7 ++++ cms/djangoapps/auth/tests/test_authz.py | 32 ++++++++++++++++++- .../management/commands/populate_creators.py | 14 ++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 cms/djangoapps/contentstore/management/commands/populate_creators.py diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index a544906875..169332e820 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -205,3 +205,10 @@ def is_user_in_creator_group(user): return user.groups.filter(name=COURSE_CREATOR_GROUP_NAME).count() > 0 return True + + +def _grant_instructors_creator_access(caller): + for group in Group.objects.all(): + if group.name.startswith(INSTRUCTOR_ROLE_NAME + "_"): + for user in group.user_set.all(): + add_user_to_creator_group(caller, user) diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 173155df4c..511ac8ead8 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -9,7 +9,7 @@ from django.core.exceptions import PermissionDenied from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group,\ create_all_course_groups, add_user_to_course_group, STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME,\ - is_user_in_course_group_role, remove_user_from_course_group + is_user_in_course_group_role, remove_user_from_course_group, _grant_instructors_creator_access class CreatorGroupTest(TestCase): @@ -174,3 +174,33 @@ class CourseGroupTest(TestCase): create_all_course_groups(self.creator, self.location) with self.assertRaises(PermissionDenied): remove_user_from_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME) + + +class GrantInstructorsCreatorAccessTest(TestCase): + + def create_course(self, index): + creator = User.objects.create_user('testcreator'+str(index), 'testcreator+courses@edx.org', 'foo') + staff = User.objects.create_user('teststaff'+str(index), 'teststaff+courses@edx.org', 'foo') + location = 'i4x', 'mitX', str(index), 'course', 'test' + create_all_course_groups(creator, location) + add_user_to_course_group(creator, staff, location, STAFF_ROLE_NAME) + return [creator, staff] + + def test_grant_creator_access(self): + [creator1, staff1] = self.create_course(1) + [creator2, staff2] = self.create_course(2) + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): + self.assertFalse(is_user_in_creator_group(creator1)) + self.assertFalse(is_user_in_creator_group(creator2)) + self.assertFalse(is_user_in_creator_group(staff1)) + self.assertFalse(is_user_in_creator_group(staff2)) + + admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo') + admin.is_staff = True + _grant_instructors_creator_access(admin) + _grant_instructors_creator_access(admin) + + self.assertTrue(is_user_in_creator_group(creator1)) + self.assertTrue(is_user_in_creator_group(creator2)) + self.assertFalse(is_user_in_creator_group(staff1)) + self.assertFalse(is_user_in_creator_group(staff2)) diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py new file mode 100644 index 0000000000..e9453025a0 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -0,0 +1,14 @@ +from auth.authz import _grant_instructors_creator_access +from django.core.management.base import BaseCommand + +from django.contrib.auth.models import User + + +class Command(BaseCommand): + help = 'Grants all users with INSTRUCTOR role permission to create courses' + + def handle(self, *args, **options): + admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo') + admin.is_staff = True + _grant_instructors_creator_access(admin) + admin.delete() From 3babf5392584350a2d34a0076b6bdaa4bb3bf521 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 09:44:09 -0400 Subject: [PATCH 023/440] Handle the case of script being terminated before the user was deleted. --- .../management/commands/populate_creators.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py index e9453025a0..28f360bacf 100644 --- a/cms/djangoapps/contentstore/management/commands/populate_creators.py +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -2,13 +2,23 @@ from auth.authz import _grant_instructors_creator_access from django.core.management.base import BaseCommand from django.contrib.auth.models import User +from django.db.utils import IntegrityError class Command(BaseCommand): help = 'Grants all users with INSTRUCTOR role permission to create courses' def handle(self, *args, **options): - admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo') - admin.is_staff = True + username = 'populate_creators_command' + email = 'grant+creator+access@edx.org' + try: + admin = User.objects.create_user(username, email, 'foo') + admin.is_staff = True + admin.save() + except IntegrityError: + # If the script did not complete the last time it was run, + # the admin user will already exist. + admin = User.objects.get(username=username, email=email) + _grant_instructors_creator_access(admin) admin.delete() From ce100bad8819ddcfb8649bfffd2e8c902ee31da4 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 09:48:47 -0400 Subject: [PATCH 024/440] Add doc strings. --- cms/djangoapps/auth/authz.py | 4 ++++ cms/djangoapps/auth/tests/test_authz.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 169332e820..bc11828b2a 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -208,6 +208,10 @@ def is_user_in_creator_group(user): def _grant_instructors_creator_access(caller): + """ + This is to be called only by either a command line code path or through an app which has already + asserted permissions to do this action + """ for group in Group.objects.all(): if group.name.startswith(INSTRUCTOR_ROLE_NAME + "_"): for user in group.user_set.all(): diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 511ac8ead8..a615a78bbc 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -177,8 +177,13 @@ class CourseGroupTest(TestCase): class GrantInstructorsCreatorAccessTest(TestCase): - + """ + Tests granting existing instructors course creator rights. + """ def create_course(self, index): + """ + Creates a course with one instructor and one staff member. + """ creator = User.objects.create_user('testcreator'+str(index), 'testcreator+courses@edx.org', 'foo') staff = User.objects.create_user('teststaff'+str(index), 'teststaff+courses@edx.org', 'foo') location = 'i4x', 'mitX', str(index), 'course', 'test' @@ -187,9 +192,13 @@ class GrantInstructorsCreatorAccessTest(TestCase): return [creator, staff] def test_grant_creator_access(self): + """ + Test for _grant_instructors_creator_access. + """ [creator1, staff1] = self.create_course(1) [creator2, staff2] = self.create_course(2) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): + # Initially no creators. self.assertFalse(is_user_in_creator_group(creator1)) self.assertFalse(is_user_in_creator_group(creator2)) self.assertFalse(is_user_in_creator_group(staff1)) @@ -198,9 +207,10 @@ class GrantInstructorsCreatorAccessTest(TestCase): admin = User.objects.create_user('populate_creators_command', 'grant+creator+access@edx.org', 'foo') admin.is_staff = True _grant_instructors_creator_access(admin) - _grant_instructors_creator_access(admin) + # Now instructors only are creators. self.assertTrue(is_user_in_creator_group(creator1)) self.assertTrue(is_user_in_creator_group(creator2)) self.assertFalse(is_user_in_creator_group(staff1)) self.assertFalse(is_user_in_creator_group(staff2)) + From 0e0f22999d9c12236b5cee755a562f5cd3a2f81c Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 09:52:18 -0400 Subject: [PATCH 025/440] Add doc strings. --- .../management/commands/populate_creators.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py index 28f360bacf..41c5d8194a 100644 --- a/cms/djangoapps/contentstore/management/commands/populate_creators.py +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -1,3 +1,6 @@ +""" +Script for granting existing course instructors course creator privileges. +""" from auth.authz import _grant_instructors_creator_access from django.core.management.base import BaseCommand @@ -6,9 +9,15 @@ from django.db.utils import IntegrityError class Command(BaseCommand): + """ + Script for granting existing course instructors course creator privileges. + """ help = 'Grants all users with INSTRUCTOR role permission to create courses' def handle(self, *args, **options): + """ + The logic of the command. + """ username = 'populate_creators_command' email = 'grant+creator+access@edx.org' try: From ea8e5f84acae87ee15eb7796e6d6be34b2902c3d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 27 Jun 2013 10:14:43 -0400 Subject: [PATCH 026/440] Cleaned up code --- .../contentstore/features/component.feature | 4 +- .../contentstore/features/component.py | 126 +++++++++--------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature index b3d446fc3b..6a18dfa7ab 100644 --- a/cms/djangoapps/contentstore/features/component.feature +++ b/cms/djangoapps/contentstore/features/component.feature @@ -3,7 +3,7 @@ Feature: Component Adding Scenario: I can add components Given I have opened a new course in studio - And I am on a new unit + And I am editing a new unit When I add the following components: | Component | | Discussion | @@ -46,7 +46,7 @@ Feature: Component Adding Scenario: I can delete Components Given I have opened a new course in studio - And I am on a new unit + And I am editing a new unit And I add the following components: | Component | | Discussion | diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py index 7366adf07c..b3775a6cbe 100644 --- a/cms/djangoapps/contentstore/features/component.py +++ b/cms/djangoapps/contentstore/features/component.py @@ -3,49 +3,34 @@ from lettuce import world, step -data_location = 'i4x://edx/templates' +DATA_LOCATION = 'i4x://edx/templates' -@step(u'I am on a new unit') +@step(u'I am editing a new unit') def add_unit(step): - section_css = 'a.new-courseware-section-button' - world.css_click(section_css) - save_section_css = 'input.new-section-name-save' - world.css_click(save_section_css) - subsection_css = 'a.new-subsection-item' - world.css_click(subsection_css) - save_subsection_css = 'input.new-subsection-name-save' - world.css_click(save_subsection_css) - expand_css = 'div.section-item a.expand-collapse-icon' - world.css_click(expand_css) - unit_css = 'a.new-unit-item' - world.css_click(unit_css) + css_selectors = ['a.new-courseware-section-button', 'input.new-section-name-save', 'a.new-subsection-item', 'input.new-subsection-name-save', 'div.section-item a.expand-collapse-icon', 'a.new-unit-item'] + for selector in css_selectors: + world.css_click(selector) @step(u'I add the following components:') def add_components(step): - for component in step.hashes: - #due to the way lettuce stores the dictionary - component = component['Component'] - #from pdb import set_trace; set_trace() - assert component in component_dictionary - how_to_add = component_dictionary[component]['steps'] - for css in how_to_add: + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + for css in COMPONENT_DICTIONARY[component]['steps']: world.css_click(css) @step(u'I see the following components') def check_components(step): - for component in step.hashes: - component = component['Component'] - assert component in component_dictionary - assert component_dictionary[component]['found']() + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + assert COMPONENT_DICTIONARY[component]['found_func']() @step(u'I delete all components') def delete_all_components(step): - components_num = len(component_dictionary) - for delete in range(0, components_num): + for _ in range(len(COMPONENT_DICTIONARY)): world.css_click('a.delete-button') @@ -54,73 +39,90 @@ def see_no_components(steps): assert world.is_css_not_present('li.component') -component_dictionary = { +def step_selector_list(data_type, path, index=1): + selector_list = ['a[data-type="{}"]'.format(data_type)] + if index != 1: + selector_list.append('a[id="ui-id-{}"]'.format(index)) + if path is not None: + selector_list.append('a[data-location="{}/{}/{}"]'.format(DATA_LOCATION, data_type, path)) + return selector_list + + +def found_text_func(text): + return lambda: world.browser.is_text_present(text) + + +def found_css_func(css): + return lambda: world.is_css_present(css, wait_time=2) + +COMPONENT_DICTIONARY = { 'Discussion': { - 'steps': ['a[data-type="discussion"]'], - 'found': lambda: world.is_css_present('section.xmodule_DiscussionModule', wait_time=2) + 'steps': step_selector_list('discussion', None), + 'found_func': found_css_func('section.xmodule_DiscussionModule') }, 'Announcement': { - 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/Announcement"]' % data_location], - 'found': lambda: world.browser.is_text_present('Heading of document') + 'steps': step_selector_list('html', 'Announcement'), + 'found_func': found_text_func('Heading of document') }, 'Blank HTML': { - 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/Blank_HTML_Page"]' % data_location], - 'found': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')] + 'steps': step_selector_list('html', 'Blank_HTML_Page'), + #this one is a blank html so a more refined search is being done + 'found_func': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')] }, 'LaTex': { - 'steps': ['a[data-type="html"]', 'a[data-location="%s/html/E-text_Written_in_LaTeX"]' % data_location], - 'found': lambda: world.browser.is_text_present('EXAMPLE: E-TEXT PAGE', wait_time=2) + 'steps': step_selector_list('html', 'E-text_Written_in_LaTeX'), + 'found_func': found_text_func('EXAMPLE: E-TEXT PAGE') }, 'Blank Problem': { - 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Blank_Common_Problem"]' % data_location], - 'found': lambda: world.browser.is_text_present('BLANK COMMON PROBLEM', wait_time=2) + 'steps': step_selector_list('problem', 'Blank_Common_Problem'), + 'found_func': found_text_func('BLANK COMMON PROBLEM') }, 'Dropdown': { - 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Dropdown"]' % data_location], - 'found': lambda: world.browser.is_text_present('DROPDOWN', wait_time=2) + 'steps': step_selector_list('problem', 'Dropdown'), + 'found_func': found_text_func('DROPDOWN') }, 'Multi Choice': { - 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Multiple_Choice"]' % data_location], - 'found': lambda: world.browser.is_text_present('MULTIPLE CHOICE', wait_time=2) + 'steps': step_selector_list('problem', 'Multiple_Choice'), + 'found_func': found_text_func('MULTIPLE CHOICE') }, 'Numerical': { - 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Numerical_Input"]' % data_location], - 'found': lambda: world.browser.is_text_present('NUMERICAL INPUT', wait_time=2) + 'steps': step_selector_list('problem', 'Numerical_Input'), + 'found_func': found_text_func('NUMERICAL INPUT') }, 'Text Input': { - 'steps': ['a[data-type="problem"]', 'a[data-location="%s/problem/Text_Input"]' % data_location], - 'found': lambda: world.browser.is_text_present('TEXT INPUT', wait_time=2) + 'steps': step_selector_list('problem', 'Text_Input'), + 'found_func': found_text_func('TEXT INPUT') }, 'Advanced': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Blank_Advanced_Problem"]' % data_location], - 'found': lambda: world.browser.is_text_present('BLANK ADVANCED PROBLEM', wait_time=2) + 'steps': step_selector_list('problem', 'Blank_Advanced_Problem', index=2), + 'found_func': found_text_func('BLANK ADVANCED PROBLEM') }, 'Circuit': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Circuit_Schematic_Builder"]' % data_location], - 'found': lambda: world.browser.is_text_present('CIRCUIT SCHEMATIC BUILDER', wait_time=2) + 'steps': step_selector_list('problem', 'Circuit_Schematic_Builder', index=2), + 'found_func': found_text_func('CIRCUIT SCHEMATIC BUILDER') }, 'Custom Python': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Custom_Python-Evaluated_Input"]' % data_location], - 'found': lambda: world.browser.is_text_present('CUSTOM PYTHON-EVALUATED INPUT', wait_time=2) + 'steps': step_selector_list('problem', 'Custom_Python-Evaluated_Input', index=2), + 'found_func': found_text_func('CUSTOM PYTHON-EVALUATED INPUT') }, 'Image Mapped': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Image_Mapped_Input"]' % data_location], - 'found': lambda: world.browser.is_text_present('IMAGE MAPPED INPUT', wait_time=2) + 'steps': step_selector_list('problem', 'Image_Mapped_Input', index=2), + 'found_func': found_text_func('IMAGE MAPPED INPUT') }, 'Math Input': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Math_Expression_Input"]' % data_location], - 'found': lambda: world.browser.is_text_present('MATH EXPRESSION INPUT', wait_time=2) + 'steps': step_selector_list('problem', 'Math_Expression_Input', index=2), + 'found_func': found_text_func('MATH EXPRESSION INPUT') }, 'Problem LaTex': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Problem_Written_in_LaTeX"]' % data_location], - 'found': lambda: world.browser.is_text_present('PROBLEM WRITTEN IN LATEX', wait_time=2) + 'steps': step_selector_list('problem', 'Problem_Written_in_LaTeX', index=2), + 'found_func': found_text_func('PROBLEM WRITTEN IN LATEX') }, 'Adaptive Hint': { - 'steps': ['a[data-type="problem"]', 'a[id="ui-id-2"]', 'a[data-location="%s/problem/Problem_with_Adaptive_Hint"]' % data_location], - 'found': lambda: world.browser.is_text_present('PROBLEM WITH ADAPTIVE HINT', wait_time=2) + 'steps': step_selector_list('problem', 'Problem_with_Adaptive_Hint', index=2), + 'found_func': found_text_func('PROBLEM WITH ADAPTIVE HINT') }, 'Video': { - 'steps': ['a[data-type="video"]'], - 'found': lambda: world.is_css_present('section.xmodule_VideoModule', wait_time=2) + 'steps': step_selector_list('video', None), + 'found_func': found_css_func('section.xmodule_VideoModule') } } From 27e720cf3b8d8802524381e61b9f94ea64b81d79 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 11:00:11 -0400 Subject: [PATCH 027/440] Make it clear that this should only be run once. --- cms/djangoapps/auth/authz.py | 5 ++++- .../contentstore/management/commands/populate_creators.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index bc11828b2a..438c9d0697 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -210,7 +210,10 @@ def is_user_in_creator_group(user): def _grant_instructors_creator_access(caller): """ This is to be called only by either a command line code path or through an app which has already - asserted permissions to do this action + asserted permissions to do this action. + + Gives all users with instructor role course creator rights. + This is only intended to be run once on a given environment. """ for group in Group.objects.all(): if group.name.startswith(INSTRUCTOR_ROLE_NAME + "_"): diff --git a/cms/djangoapps/contentstore/management/commands/populate_creators.py b/cms/djangoapps/contentstore/management/commands/populate_creators.py index 41c5d8194a..f627df88f5 100644 --- a/cms/djangoapps/contentstore/management/commands/populate_creators.py +++ b/cms/djangoapps/contentstore/management/commands/populate_creators.py @@ -1,5 +1,7 @@ """ Script for granting existing course instructors course creator privileges. + +This script is only intended to be run once on a given environment. """ from auth.authz import _grant_instructors_creator_access from django.core.management.base import BaseCommand From f095c5fec6507ba38552b0e02530e8ce981ffcbe Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 11:00:43 -0400 Subject: [PATCH 028/440] Add ENABLE_CREATOR_GROUP (set to False). --- cms/envs/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 7f4c106e6d..87c130a4b5 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -54,7 +54,11 @@ MITX_FEATURES = { 'ENABLE_SERVICE_STATUS': False, # Don't autoplay videos for course authors - 'AUTOPLAY_VIDEOS': False + 'AUTOPLAY_VIDEOS': False, + + # If set to True, new Studio users won't be able to author courses unless + # edX has explicitly added them to the course creator group. + 'ENABLE_CREATOR_GROUP': False } ENABLE_JASMINE = False From 8d4c2af9f2dcf8e4a2a600cfe239709f1fdd25ee Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 27 Jun 2013 13:34:58 -0400 Subject: [PATCH 029/440] Teach rake test_$lib to understand specific test names --- rakelib/tests.rake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index 0ca7c5c1e9..48ad3ad1d5 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -99,9 +99,10 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" - task "test_#{lib}" => [report_dir, :clean_reports_dir] do + task "test_#{lib}", [:test_id] => [report_dir, :clean_reports_dir] do |t, args| + args.with_defaults(:test_id => lib) ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") - cmd = "nosetests #{lib}" + cmd = "nosetests #{args.test_id}" test_sh(run_under_coverage(cmd, lib)) end TEST_TASK_DIRS << lib From f6d7cc55166b06ddabe97decd6780d8ebf6cba1e Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Thu, 27 Jun 2013 15:57:51 -0400 Subject: [PATCH 030/440] save the cert record immediately after we set the status There was a potential though unlikely race condition because the .save() was after the certificate request was put on the queue. Now calling .save() immediately after we update the cert. --- lms/djangoapps/certificates/queue.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index af1037f903..78e786e884 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -194,6 +194,7 @@ class XQueueCertInterface(object): # on the queue if self.restricted.filter(user=student).exists(): cert.status = status.restricted + cert.save() else: contents = { 'action': 'create', @@ -202,15 +203,15 @@ class XQueueCertInterface(object): 'name': profile.name, } cert.status = status.generating + cert.save() self._send_to_xqueue(contents, key) - cert.save() else: cert_status = status.notpassing cert.grade = grade['percent'] - cert.status = cert_status cert.user = student cert.course_id = course_id cert.name = profile.name + cert.status = cert_status cert.save() return cert_status From 32e6d4819f5a4d1f879b3b33e3b1f81940ea2736 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 27 Jun 2013 16:18:16 -0400 Subject: [PATCH 031/440] pep8/pylint cleanup. --- cms/djangoapps/auth/authz.py | 4 +++- cms/djangoapps/auth/tests/test_authz.py | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 438c9d0697..23dde88e94 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -36,7 +36,7 @@ def get_course_groupname_for_role(location, role): def get_users_in_course_group_by_role(location, role): groupname = get_course_groupname_for_role(location, role) - (group, created) = Group.objects.get_or_create(name=groupname) + (group, _created) = Group.objects.get_or_create(name=groupname) return group.user_set.all() @@ -59,6 +59,7 @@ def create_new_course_group(creator, location, role): return + def _delete_course_group(location): """ This is to be called only by either a command line code path or through a app which has already @@ -75,6 +76,7 @@ def _delete_course_group(location): user.groups.remove(staff) user.save() + def _copy_course_group(source, dest): """ This is to be called only by either a command line code path or through an app which has already diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index a615a78bbc..658c176498 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -184,8 +184,8 @@ class GrantInstructorsCreatorAccessTest(TestCase): """ Creates a course with one instructor and one staff member. """ - creator = User.objects.create_user('testcreator'+str(index), 'testcreator+courses@edx.org', 'foo') - staff = User.objects.create_user('teststaff'+str(index), 'teststaff+courses@edx.org', 'foo') + creator = User.objects.create_user('testcreator' + str(index), 'testcreator+courses@edx.org', 'foo') + staff = User.objects.create_user('teststaff' + str(index), 'teststaff+courses@edx.org', 'foo') location = 'i4x', 'mitX', str(index), 'course', 'test' create_all_course_groups(creator, location) add_user_to_course_group(creator, staff, location, STAFF_ROLE_NAME) @@ -213,4 +213,3 @@ class GrantInstructorsCreatorAccessTest(TestCase): self.assertTrue(is_user_in_creator_group(creator2)) self.assertFalse(is_user_in_creator_group(staff1)) self.assertFalse(is_user_in_creator_group(staff2)) - From 4557891e0de803e1d6236387371ff8788df918df Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Thu, 27 Jun 2013 16:25:03 -0400 Subject: [PATCH 032/440] Standardize types used for logging events from the instructor dashboard --- lms/djangoapps/instructor/views.py | 87 +++++++++++------------------- 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index ea96901bca..60c4f2143d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -200,7 +200,7 @@ def instructor_dashboard(request, course_id): cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:

".format(data_dir) msg += "

{0}

".format(escape(os.popen(cmd).read())) - track.views.server_track(request, 'git pull {0}'.format(data_dir), {}, page='idashboard') + track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") if 'Reload course' in action: log.debug('reloading {0} ({1})'.format(course_id, course)) @@ -208,7 +208,7 @@ def instructor_dashboard(request, course_id): data_dir = getattr(course, 'data_dir') modulestore().try_load_course(data_dir) msg += "

Course reloaded from {0}

".format(data_dir) - track.views.server_track(request, 'reload {0}'.format(data_dir), {}, page='idashboard') + track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_item_errors(course.location) msg += ' + +
+

You haven't added any textbooks to this course yet.

+

Add your first textbook

+
+ + + + + From f198b7e297b0f4f8ff065512bd8b9b678814ce43 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 31 May 2013 13:37:27 -0400 Subject: [PATCH 269/440] Clean up Backbone view logic --- cms/templates/js/upload-dialog.underscore | 7 +++++- cms/templates/textbooks.html | 28 +++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cms/templates/js/upload-dialog.underscore b/cms/templates/js/upload-dialog.underscore index 0270525541..5994cabcb8 100644 --- a/cms/templates/js/upload-dialog.underscore +++ b/cms/templates/js/upload-dialog.underscore @@ -1,4 +1,9 @@ -