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/161] 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/161] 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/161] 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/161] 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 7b074424b540db5ad9b2c0c5840de04162680134 Mon Sep 17 00:00:00 2001
From: David Ormsbee
Date: Wed, 19 Jun 2013 14:39:02 -0400
Subject: [PATCH 005/161] Initialize MakoMiddleware manually during certificate
grading runs.
Without this, problems fail to load because they can't find how to
render themselves, and the certificate generation grading run will
get an inaccurately low count of the possible points a user could
get (anything they didn't see will be omitted), inflating their
grade during certificate calculation and making it inconsistent
with their Progress page.
---
lms/djangoapps/certificates/queue.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py
index b4632ce9ab..af1037f903 100644
--- a/lms/djangoapps/certificates/queue.py
+++ b/lms/djangoapps/certificates/queue.py
@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status
from certificates.models import CertificateWhitelist
+from mitxmako.middleware import MakoMiddleware
from courseware import grades, courses
from django.test.client import RequestFactory
from capa.xqueue_interface import XQueueInterface
@@ -51,6 +52,14 @@ class XQueueCertInterface(object):
"""
def __init__(self, request=None):
+ # MakoMiddleware Note:
+ # Line below has the side-effect of writing to a module level lookup
+ # table that will allow problems to render themselves. If this is not
+ # present, problems that a student hasn't seen will error when loading,
+ # causing the grading system to under-count the possible score and
+ # inflate their grade. This dependency is bad and was probably recently
+ # introduced. This is the bandage until we can trace the root cause.
+ m = MakoMiddleware()
# Get basic auth (username/password) for
# xqueue connection if it's in the settings
@@ -161,6 +170,10 @@ class XQueueCertInterface(object):
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
+ # Needed
+ self.request.user = student
+ self.request.session = {}
+
grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists()
@@ -211,5 +224,5 @@ class XQueueCertInterface(object):
(error, msg) = self.xqueue_interface.send_to_queue(
header=xheader, body=json.dumps(contents))
if error:
- logger.critical('Unable to add a request to the queue')
+ logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
raise Exception('Unable to send queue message')
From 4f78c1977f2256fc832514256f4f967040f7eaff Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Wed, 19 Jun 2013 10:59:24 -0400
Subject: [PATCH 006/161] Allow error messages with non-ascii characters to be
handled correctly
Also, add a test for this behavior.
---
CHANGELOG.rst | 2 ++
common/lib/xmodule/xmodule/capa_module.py | 2 +-
.../xmodule/xmodule/tests/test_capa_module.py | 28 +++++++++++++++++++
3 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 206be44c87..6a79757c0f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -42,6 +42,8 @@ setting now run entirely outside the Python sandbox.
Blades: Added tests for Video Alpha player.
+Common: Have the capa module handle unicode better (especially errors)
+
Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox.
Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index d9f7fc61aa..85c935c9e7 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -781,7 +781,7 @@ class CapaModule(CapaFields, XModule):
# Otherwise, display just an error message,
# without a stack trace
else:
- msg = "Error: %s" % str(inst.message)
+ msg = u"Error: {msg}".format(msg=inst.message)
return {'success': msg}
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 696ef58268..85e69cabc1 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
"""Tests of the Capa XModule"""
#pylint: disable=C0111
#pylint: disable=R0904
@@ -520,6 +521,33 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1)
+ def test_check_problem_error_nonascii(self):
+
+ # Try each exception that capa_module should handle
+ for exception_class in [StudentInputError,
+ LoncapaProblemError,
+ ResponseError]:
+
+ # Create the module
+ module = CapaFactory.create(attempts=1)
+
+ # Ensure that the user is NOT staff
+ module.system.user_is_staff = False
+
+ # Simulate answering a problem that raises the exception
+ with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
+ mock_grade.side_effect = exception_class(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř Å§á¸—ÅŸÅ§Ä«ÆžÉ ")
+
+ get_request_dict = {CapaFactory.input_key(): '3.14'}
+ result = module.check_problem(get_request_dict)
+
+ # Expect an AJAX alert message in 'success'
+ expected_msg = u'Error: ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř Å§á¸—ÅŸÅ§Ä«ÆžÉ '
+ self.assertEqual(expected_msg, result['success'])
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
+
def test_check_problem_error_with_staff_user(self):
# Try each exception that capa module should handle
From 401dd550e477ca0616313f85aa2f64d64dc88a2b Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Tue, 18 Jun 2013 13:14:52 -0400
Subject: [PATCH 007/161] Convert many byte strings to unicode; change string
formatting
---
common/lib/calc/calc.py | 2 +-
common/lib/xmodule/xmodule/capa_module.py | 49 +++++++++++++----------
2 files changed, 28 insertions(+), 23 deletions(-)
diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py
index f0934a9ed5..bbfd9545f6 100644
--- a/common/lib/calc/calc.py
+++ b/common/lib/calc/calc.py
@@ -93,7 +93,7 @@ def check_variables(string, variables):
Pyparsing uses a left-to-right parser, which makes a more
elegant approach pretty hopeless.
"""
- general_whitespace = re.compile('[^\\w]+')
+ general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii
# List of all alnums in string
possible_variables = re.split(general_whitespace, string)
bad_variables = []
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 85c935c9e7..3bd8331678 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -60,7 +60,7 @@ class Randomization(String):
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
- return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
+ return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
return json.JSONEncoder.default(self, obj)
@@ -167,7 +167,7 @@ class CapaModule(CapaFields, XModule):
self.seed = self.lcp.seed
except Exception as err:
- msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
+ msg = u'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
# TODO (vshnayder): do modules need error handlers too?
# We shouldn't be switching on DEBUG.
@@ -176,12 +176,15 @@ class CapaModule(CapaFields, XModule):
# TODO (vshnayder): This logic should be general, not here--and may
# want to preserve the data instead of replacing it.
# e.g. in the CMS
- msg = '%s
' % msg.replace('<', '<')
- msg += '%s
' % traceback.format_exc().replace('<', '<')
+ msg = u'{msg}
'.format(msg=cgi.escape(msg))
+ msg += u'{tb}'.format(
+ tb=cgi.escape(traceback.format_exc()))
# create a dummy problem with error message instead of failing
- problem_text = (''
- 'Problem %s has an error:%s' %
- (self.location.url(), msg))
+ problem_text = (u''
+ u'Problem {url} has an error:{msg}'.format(
+ url=self.location.url(),
+ msg=msg)
+ )
self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text)
else:
# add extra info and raise
@@ -362,15 +365,14 @@ class CapaModule(CapaFields, XModule):
# TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG:
msg = (
- '[courseware.capa.capa_module] '
- 'Failed to generate HTML for problem %s' %
- (self.location.url()))
- msg += 'Error:
%s
' % str(err).replace('<', '<')
- msg += '%s
' % traceback.format_exc().replace('<', '<')
+ u'[courseware.capa.capa_module] '
+ u'Failed to generate HTML for problem {url}'.format(
+ url=cgi.escape(self.location.url()))
+ )
+ msg += u'Error:
{msg}'.format(msg=cgi.escape(err.message))
+ msg += u'{tb}'.format(tb=cgi.escape(traceback.format_exc()))
html = msg
- # We're in non-debug mode, and possibly even in production. We want
- # to avoid bricking of problem as much as possible
else:
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
@@ -454,8 +456,9 @@ class CapaModule(CapaFields, XModule):
html = self.system.render_template('problem.html', context)
if encapsulate:
- html = ''.format(
- id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
"
+ html = u''.format(
+ id=self.location.html_id(), ajax_url=self.system.ajax_url
+ ) + html + "
"
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html)
@@ -641,7 +644,8 @@ class CapaModule(CapaFields, XModule):
try:
new_answer = {answer_id: self.system.replace_urls(answers[answer_id])}
except TypeError:
- log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
+ log.debug(u'Unable to perform URL substitution on answers[%s]: %s',
+ answer_id, answers[answer_id])
new_answer = {answer_id: answers[answer_id]}
new_answers.update(new_answer)
@@ -693,7 +697,7 @@ class CapaModule(CapaFields, XModule):
# will return (key, '', '')
# We detect this and raise an error
if not name:
- raise ValueError("%s must contain at least one underscore" % str(key))
+ raise ValueError(u"{key} must contain at least one underscore".format(key=key))
else:
# This allows for answers which require more than one value for
@@ -711,7 +715,7 @@ class CapaModule(CapaFields, XModule):
# If the name already exists, then we don't want
# to override it. Raise an error instead
if name in answers:
- raise ValueError("Key %s already exists in answers dict" % str(name))
+ raise ValueError(u"Key {name} already exists in answers dict".format(name=name))
else:
answers[name] = val
@@ -759,7 +763,8 @@ class CapaModule(CapaFields, XModule):
prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime']
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
- msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
+ msg = u'You must wait at least {wait} seconds between submissions'.format(
+ wait=waittime_between_requests)
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try:
@@ -776,7 +781,7 @@ class CapaModule(CapaFields, XModule):
# the full exception, including traceback,
# in the response
if self.system.user_is_staff:
- msg = "Staff debug info: %s" % traceback.format_exc()
+ msg = u"Staff debug info: {tb}".format(tb=cgi.escape(traceback.format_exc()))
# Otherwise, display just an error message,
# without a stack trace
@@ -787,7 +792,7 @@ class CapaModule(CapaFields, XModule):
except Exception as err:
if self.system.DEBUG:
- msg = "Error checking problem: " + str(err)
+ msg = "Error checking problem: " + err.message
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg}
raise
From b68e1e207e3fb99980de4eb8bf8b904a7ceabb13 Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Tue, 18 Jun 2013 13:24:22 -0400
Subject: [PATCH 008/161] Fix some line lengths to make pylint happy
---
common/lib/xmodule/xmodule/capa_module.py | 45 ++++++++++++++++-------
1 file changed, 31 insertions(+), 14 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 3bd8331678..40f685baee 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -22,7 +22,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date
from django.utils.timezone import UTC
-log = logging.getLogger("mitx.courseware")
+log = logging.getLogger("mitx.courseware") # pylint: disable=C0103
# Generate this many different variants of problems with rerandomize=per_student
@@ -65,17 +65,23 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object):
- attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
+ attempts = Integer(help="Number of attempts taken by the student on this problem",
+ default=0, scope=Scope.user_state)
max_attempts = Integer(
display_name="Maximum Attempts",
- help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
+ help=("Defines the number of times a student can try to answer this problem. "
+ "If the value is not set, infinite attempts are allowed."),
values={"min": 0}, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
- graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
+ graceperiod = Timedelta(
+ help="Amount of time after the due date that submissions will be accepted",
+ scope=Scope.settings
+ )
showanswer = String(
display_name="Show Answer",
- help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
+ help=("Defines when to show the answer to the problem. "
+ "A default value can be set in Advanced Settings."),
scope=Scope.settings, default="closed",
values=[
{"display_name": "Always", "value": "always"},
@@ -86,23 +92,33 @@ class CapaFields(object):
{"display_name": "Past Due", "value": "past_due"},
{"display_name": "Never", "value": "never"}]
)
- force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
+ force_save_button = Boolean(
+ help="Whether to force the save button to appear on the page",
+ scope=Scope.settings, default=False
+ )
rerandomize = Randomization(
- display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
- default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
- {"display_name": "On Reset", "value": "onreset"},
- {"display_name": "Never", "value": "never"},
- {"display_name": "Per Student", "value": "per_student"}]
+ display_name="Randomization",
+ help="Defines how often inputs are randomized when a student loads the problem. "
+ "This setting only applies to problems that can have randomly generated numeric values. "
+ "A default value can be set in Advanced Settings.",
+ default="always", scope=Scope.settings, values=[
+ {"display_name": "Always", "value": "always"},
+ {"display_name": "On Reset", "value": "onreset"},
+ {"display_name": "Never", "value": "never"},
+ {"display_name": "Per Student", "value": "per_student"}
+ ]
)
data = String(help="XML data for the problem", scope=Scope.content)
- correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
+ correct_map = Dict(help="Dictionary with the correctness of current student answers",
+ scope=Scope.user_state, default={})
input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = Integer(help="Random seed for this student", scope=Scope.user_state)
weight = Float(
display_name="Problem Weight",
- help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
+ help=("Defines the number of points each problem is worth. "
+ "If the value is not set, each response field in the problem is worth one point."),
values={"min": 0, "step": .1},
scope=Scope.settings
)
@@ -998,7 +1014,8 @@ class CapaDescriptor(CapaFields, RawDescriptor):
mako_template = "widgets/problem-edit.html"
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
js_module_name = "MarkdownEditingDescriptor"
- css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/problem/edit.scss')]}
+ css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'),
+ resource_string(__name__, 'css/problem/edit.scss')]}
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they
From f623e42983545f99a0cf7bd69e7bccccb55e285e Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Tue, 18 Jun 2013 15:48:55 -0400
Subject: [PATCH 009/161] Fix formatting of docstrings; add more docstrings
---
common/lib/xmodule/xmodule/capa_module.py | 170 ++++++++++++------
.../xmodule/xmodule/tests/test_capa_module.py | 16 +-
2 files changed, 126 insertions(+), 60 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 40f685baee..b927106b4a 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -47,7 +47,13 @@ def randomization_bin(seed, problem_id):
class Randomization(String):
+ """
+ Define a field to store how to randomize a problem.
+ """
def from_json(self, value):
+ """
+ For backward compatability?
+ """
if value in ("", "true"):
return "always"
elif value == "false":
@@ -58,13 +64,22 @@ class Randomization(String):
class ComplexEncoder(json.JSONEncoder):
+ """
+ Extend the JSON encoder to correctly handle complex numbers
+ """
def default(self, obj):
+ """
+ Print a nicely formatted complex number, or default to the JSON encoder
+ """
if isinstance(obj, complex):
return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
return json.JSONEncoder.default(self, obj)
class CapaFields(object):
+ """
+ Define the possible fields for a Capa problem
+ """
attempts = Integer(help="Number of attempts taken by the student on this problem",
default=0, scope=Scope.user_state)
max_attempts = Integer(
@@ -130,12 +145,12 @@ class CapaFields(object):
class CapaModule(CapaFields, XModule):
- '''
+ """
An XModule implementing LonCapa format problems, implemented by way of
capa.capa_problem.LoncapaProblem
CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
- '''
+ """
icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
@@ -150,7 +165,9 @@ class CapaModule(CapaFields, XModule):
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, *args, **kwargs):
- """ Accepts the same arguments as xmodule.x_module:XModule.__init__ """
+ """
+ Accepts the same arguments as xmodule.x_module:XModule.__init__
+ """
XModule.__init__(self, *args, **kwargs)
due_date = self.due
@@ -211,7 +228,9 @@ class CapaModule(CapaFields, XModule):
assert self.seed is not None
def choose_new_seed(self):
- """Choose a new seed."""
+ """
+ Choose a new seed.
+ """
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
@@ -225,6 +244,9 @@ class CapaModule(CapaFields, XModule):
self.seed %= MAX_RANDOMIZATION_BINS
def new_lcp(self, state, text=None):
+ """
+ Generate a new Loncapa Problem
+ """
if text is None:
text = self.data
@@ -237,6 +259,9 @@ class CapaModule(CapaFields, XModule):
)
def get_state_for_lcp(self):
+ """
+ Give a dictionary holding the state of the module
+ """
return {
'done': self.done,
'correct_map': self.correct_map,
@@ -246,6 +271,9 @@ class CapaModule(CapaFields, XModule):
}
def set_state_from_lcp(self):
+ """
+ Set the module's state from the settings in `self.lcp`
+ """
lcp_state = self.lcp.get_state()
self.done = lcp_state['done']
self.correct_map = lcp_state['correct_map']
@@ -254,26 +282,36 @@ class CapaModule(CapaFields, XModule):
self.seed = lcp_state['seed']
def get_score(self):
+ """
+ Access the problem's score
+ """
return self.lcp.get_score()
def max_score(self):
+ """
+ Access the problem's max score
+ """
return self.lcp.get_max_score()
def get_progress(self):
- ''' For now, just return score / max_score
- '''
+ """
+ For now, just return score / max_score
+ """
d = self.get_score()
score = d['score']
total = d['total']
if total > 0:
try:
return Progress(score, total)
- except Exception:
+ except (TypeError, ValueError):
log.exception("Got bad progress")
return None
return None
def get_html(self):
+ """
+ Return some html with data about the module
+ """
return self.system.render_template('problem_ajax.html', {
'element_id': self.location.html_id(),
'id': self.id,
@@ -284,6 +322,7 @@ class CapaModule(CapaFields, XModule):
def check_button_name(self):
"""
Determine the name for the "check" button.
+
Usually it is just "Check", but if this is the student's
final attempt, change the name to "Final Check"
"""
@@ -369,12 +408,12 @@ class CapaModule(CapaFields, XModule):
def handle_problem_html_error(self, err):
"""
- Change our problem to a dummy problem containing
- a warning message to display to users.
+ Create a dummy problem to represent any errors.
- Returns the HTML to show to users
+ Change our problem to a dummy problem containing a warning message to
+ display to users. Returns the HTML to show to users
- *err* is the Exception encountered while rendering the problem HTML.
+ `err` is the Exception encountered while rendering the problem HTML.
"""
log.exception(err)
@@ -434,8 +473,12 @@ class CapaModule(CapaFields, XModule):
return html
def get_problem_html(self, encapsulate=True):
- '''Return html for the problem. Adds check, reset, save buttons
- as necessary based on the problem config and state.'''
+ """
+ Return html for the problem.
+
+ Adds check, reset, save buttons as necessary based on the problem config
+ and state.
+ """
try:
html = self.lcp.get_html()
@@ -480,15 +523,16 @@ class CapaModule(CapaFields, XModule):
return self.system.replace_urls(html)
def handle_ajax(self, dispatch, get):
- '''
+ """
This is called by courseware.module_render, to handle an AJAX call.
- "get" is request.POST.
+
+ `get` is request.POST.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
}
- '''
+ """
handlers = {
'problem_get': self.get_problem,
'problem_check': self.check_problem,
@@ -527,7 +571,9 @@ class CapaModule(CapaFields, XModule):
datetime.datetime.now(UTC()) > self.close_date)
def closed(self):
- ''' Is the student still allowed to submit answers? '''
+ """
+ Is the student still allowed to submit answers?
+ """
if self.max_attempts is not None and self.attempts >= self.max_attempts:
return True
if self.is_past_due():
@@ -546,18 +592,24 @@ class CapaModule(CapaFields, XModule):
return self.lcp.done
def is_attempted(self):
- """Used by conditional module"""
+ """
+ Has the problem been attempted?
+
+ used by conditional module
+ """
return self.attempts > 0
def is_correct(self):
- """True if full points"""
+ """
+ True iff full points
+ """
d = self.get_score()
return d['score'] == d['total']
def answer_available(self):
- '''
+ """
Is the user allowed to see an answer?
- '''
+ """
if self.showanswer == '':
return False
elif self.showanswer == "never":
@@ -589,7 +641,7 @@ class CapaModule(CapaFields, XModule):
Delivers grading response (e.g. from asynchronous code checking) to
the capa problem, so its score can be updated
- 'get' must have a field 'response' which is a string that contains the
+ `get` must have a field `response` which is a string that contains the
grader's response
No ajax return is needed. Return empty dict.
@@ -603,7 +655,7 @@ class CapaModule(CapaFields, XModule):
return dict() # No AJAX return is needed
def handle_ungraded_response(self, get):
- '''
+ """
Delivers a response from the XQueue to the capa problem
The score of the problem will not be updated
@@ -616,7 +668,7 @@ class CapaModule(CapaFields, XModule):
empty dictionary
No ajax return is needed, so an empty dict is returned
- '''
+ """
queuekey = get['queuekey']
score_msg = get['xqueue_body']
# pass along the xqueue message to the problem
@@ -625,25 +677,25 @@ class CapaModule(CapaFields, XModule):
return dict()
def handle_input_ajax(self, get):
- '''
+ """
Handle ajax calls meant for a particular input in the problem
Args:
- get (dict) - data that should be passed to the input
Returns:
- dict containing the response from the input
- '''
+ """
response = self.lcp.handle_input_ajax(get)
# save any state changes that may occur
self.set_state_from_lcp()
return response
def get_answer(self, get):
- '''
+ """
For the "show answer" button.
Returns the answers: {'answers' : answers}
- '''
+ """
event_info = dict()
event_info['problem_id'] = self.location.url()
self.system.track_function('showanswer', event_info)
@@ -669,40 +721,44 @@ class CapaModule(CapaFields, XModule):
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
- ''' Return results of get_problem_html, as a simple dict for json-ing.
+ """
+ Return results of get_problem_html, as a simple dict for json-ing.
+
{ 'html': }
- Used if we want to reconfirm we have the right thing e.g. after
- several AJAX calls.
- '''
+ Used if we want to reconfirm we have the right thing e.g. after
+ several AJAX calls.
+ """
return {'html': self.get_problem_html(encapsulate=False)}
@staticmethod
def make_dict_of_responses(get):
- '''Make dictionary of student responses (aka "answers")
- get is POST dictionary (Django QueryDict).
+ """
+ Make dictionary of student responses (aka "answers")
- The *get* dict has keys of the form 'x_y', which are mapped
+ `get` is POST dictionary (Django QueryDict).
+
+ The `get` dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example,
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
Some inputs always expect a list in the returned dict
(e.g. checkbox inputs). The convention is that
- keys in the *get* dict that end with '[]' will always
+ keys in the `get` dict that end with '[]' will always
have list values in the returned dict.
- For example, if the *get* dict contains {'input_1[]': 'test' }
+ For example, if the `get` dict contains {'input_1[]': 'test' }
then the output dict would contain {'1': ['test'] }
(the value is a list).
Raises an exception if:
- A key in the *get* dictionary does not contain >= 1 underscores
- (e.g. "input" is invalid; "input_1" is valid)
+ -A key in the `get` dictionary does not contain at least one underscore
+ (e.g. "input" is invalid, but "input_1" is valid)
- Two keys end up with the same name in the returned dict.
- (e.g. 'input_1' and 'input_1[]', which both get mapped
- to 'input_1' in the returned dict)
- '''
+ -Two keys end up with the same name in the returned dict.
+ (e.g. 'input_1' and 'input_1[]', which both get mapped to 'input_1'
+ in the returned dict)
+ """
answers = dict()
for key in get:
@@ -749,12 +805,13 @@ class CapaModule(CapaFields, XModule):
})
def check_problem(self, get):
- ''' Checks whether answers to a problem are correct, and
- returns a map of correct/incorrect answers:
+ """
+ Checks whether answers to a problem are correct
- {'success' : 'correct' | 'incorrect' | AJAX alert msg string,
- 'contents' : html}
- '''
+ Returns a map of correct/incorrect answers:
+ {'success' : 'correct' | 'incorrect' | AJAX alert msg string,
+ 'contents' : html}
+ """
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
@@ -958,16 +1015,17 @@ class CapaModule(CapaFields, XModule):
'msg': msg}
def reset_problem(self, get):
- ''' Changes problem state to unfinished -- removes student answers,
- and causes problem to rerender itself.
+ """
+ Changes problem state to unfinished -- removes student answers,
+ and causes problem to rerender itself.
- Returns a dictionary of the form:
- {'success': True/False,
- 'html': Problem HTML string }
+ Returns a dictionary of the form:
+ {'success': True/False,
+ 'html': Problem HTML string }
- If an error occurs, the dictionary will also have an
- 'error' key containing an error message.
- '''
+ If an error occurs, the dictionary will also have an
+ `error` key containing an error message.
+ """
event_info = dict()
event_info['old_state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 85e69cabc1..81df686015 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
-"""Tests of the Capa XModule"""
+"""
+Tests of the Capa XModule
+"""
#pylint: disable=C0111
#pylint: disable=R0904
#pylint: disable=C0103
@@ -48,12 +50,16 @@ class CapaFactory(object):
@staticmethod
def input_key():
- """ Return the input key to use when passing GET parameters """
+ """
+ Return the input key to use when passing GET parameters
+ """
return ("input_" + CapaFactory.answer_key())
@staticmethod
def answer_key():
- """ Return the key stored in the capa problem answer dict """
+ """
+ Return the key stored in the capa problem answer dict
+ """
return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
'SampleProblem%d' % CapaFactory.num]) +
"_2_1")
@@ -362,7 +368,9 @@ class CapaModuleTest(unittest.TestCase):
result = CapaModule.make_dict_of_responses(invalid_get_dict)
def _querydict_from_dict(self, param_dict):
- """ Create a Django QueryDict from a Python dictionary """
+ """
+ Create a Django QueryDict from a Python dictionary
+ """
# QueryDict objects are immutable by default, so we make
# a copy that we can update.
From e4af7287b6f204dc759f1e9a349bf29a6864a025 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Tue, 18 Jun 2013 14:04:43 -0400
Subject: [PATCH 010/161] Initial testing for parallelization
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 6 ++++++
cms/envs/test.py | 2 +-
.../lib/xmodule/xmodule/modulestore/tests/django_utils.py | 1 +
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index d24deacecf..86699ef479 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -43,10 +43,13 @@ from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
import datetime
from pytz import UTC
+#from uuid import uuid4
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
+TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex
class MongoCollectionFindWrapper(object):
@@ -60,6 +63,7 @@ class MongoCollectionFindWrapper(object):
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
+@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
Tests that rely on the toy courses.
@@ -83,6 +87,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
+
def check_components_on_page(self, component_types, expected_types):
"""
Ensure that the right types end up on the page.
@@ -809,6 +814,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
+@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
diff --git a/cms/envs/test.py b/cms/envs/test.py
index 954a553e10..89813dd937 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -70,7 +70,7 @@ CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
- 'db': 'test_xmodule',
+ 'db': 'test_xcontent',
},
# allow for additional options that can be keyed on a name, e.g. 'trashcan'
'ADDITIONAL_OPTIONS': {
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
index 04e79ce521..e0e5c1a46f 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
@@ -27,6 +27,7 @@ class ModuleStoreTestCase(TestCase):
# Remove everything except templates
modulestore.collection.remove(query)
+ modulestore.collection.drop()
@staticmethod
def load_templates_if_necessary():
From f90ed69cd792091c9f2d57bce1cbafe0efd51094 Mon Sep 17 00:00:00 2001
From: Jay Zoldak
Date: Tue, 18 Jun 2013 15:09:53 -0400
Subject: [PATCH 011/161] move override of MODULESTORE settings into
ModuleStore test case class
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 86699ef479..9c3ec2e3ba 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -45,9 +45,7 @@ import datetime
from pytz import UTC
#from uuid import uuid4
-TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
-TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
-TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex
@@ -62,7 +60,7 @@ class MongoCollectionFindWrapper(object):
return self.original(query, *args, **kwargs)
-@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
+#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
@@ -70,6 +68,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
TODO: refactor using CourseFactory so they do not.
"""
def setUp(self):
+
+ settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
+ settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
@@ -88,6 +89,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
+
def check_components_on_page(self, component_types, expected_types):
"""
Ensure that the right types end up on the page.
From 51f8c0cfebedb7807b04ef849cfb806a0dcdba0e Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Wed, 19 Jun 2013 11:27:22 -0400
Subject: [PATCH 012/161] Added the beginnings of self cleanup
---
.../contentstore/tests/test_contentstore.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 9c3ec2e3ba..46d6a069ce 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -43,11 +43,12 @@ from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
import datetime
from pytz import UTC
-#from uuid import uuid4
+from uuid import uuid4
+import pymongo
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
-TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex
+TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
class MongoCollectionFindWrapper(object):
@@ -88,7 +89,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
-
+ def tearDown(self):
+ m = pymongo.MongoClient()
+ m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
+ #contentstore().fs_files.drop()
def check_components_on_page(self, component_types, expected_types):
"""
@@ -449,7 +453,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore()
trash_store = contentstore('trashcan')
module_store = modulestore('direct')
-
import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store)
# look up original (and thumbnail) in content store, should be there after import
@@ -853,6 +856,11 @@ class ContentStoreTest(ModuleStoreTestCase):
'display_name': 'Robot Super Course',
}
+ def tearDown(self):
+ m = pymongo.MongoClient()
+ m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
+ #contentstore().fs_files.drop()
+
def test_create_course(self):
"""Test new course creation - happy path"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
From fa18b48f6eaec45bc65f16f9585fa2555462ad55 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Thu, 20 Jun 2013 09:05:35 -0400
Subject: [PATCH 013/161] Contentstore singleton is now cleared during teardown
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 46d6a069ce..b0cbcee032 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -41,6 +41,7 @@ from xmodule.exceptions import NotFoundError
from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
+import xmodule.contentstore.django
import datetime
from pytz import UTC
from uuid import uuid4
@@ -92,7 +93,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def tearDown(self):
m = pymongo.MongoClient()
m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
- #contentstore().fs_files.drop()
+ xmodule.contentstore.django._CONTENTSTORE.clear()
def check_components_on_page(self, component_types, expected_types):
"""
@@ -859,7 +860,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def tearDown(self):
m = pymongo.MongoClient()
m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
- #contentstore().fs_files.drop()
+ xmodule.contentstore.django._CONTENTSTORE.clear()
def test_create_course(self):
"""Test new course creation - happy path"""
From cb04f9f0b82dfe46777ceb0584fadb656f7b6780 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Thu, 20 Jun 2013 17:10:36 -0400
Subject: [PATCH 014/161] Moved port range to rake file
---
jenkins/test.sh | 3 ---
rakelib/tests.rake | 2 +-
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/jenkins/test.sh b/jenkins/test.sh
index e5ac4f6f71..2ff32a9911 100755
--- a/jenkins/test.sh
+++ b/jenkins/test.sh
@@ -60,9 +60,6 @@ fi
export PIP_DOWNLOAD_CACHE=/mnt/pip-cache
-# Allow django liveserver tests to use a range of ports
-export DJANGO_LIVE_TEST_SERVER_ADDRESS=${DJANGO_LIVE_TEST_SERVER_ADDRESS-localhost:8000-9000}
-
source /mnt/virtualenvs/"$JOB_NAME"/bin/activate
bundle install
diff --git a/rakelib/tests.rake b/rakelib/tests.rake
index f169d28256..c0592cca7a 100644
--- a/rakelib/tests.rake
+++ b/rakelib/tests.rake
@@ -16,7 +16,7 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
test_id = dirs.join(' ') if test_id.nil? or test_id == ''
- cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id)
+ cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', '--liveserver=localhost:8000-9000', test_id)
test_sh(run_under_coverage(cmd, system))
end
From 7db93976c5860cf818bc915bb890b1a9c18b6838 Mon Sep 17 00:00:00 2001
From: Peter Baratta
Date: Fri, 21 Jun 2013 11:02:25 -0400
Subject: [PATCH 015/161] PR fixes
---
common/lib/xmodule/xmodule/capa_module.py | 9 +++------
.../lib/xmodule/xmodule/tests/test_capa_module.py | 14 ++++++++------
2 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index b927106b4a..d740a73946 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -22,7 +22,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date
from django.utils.timezone import UTC
-log = logging.getLogger("mitx.courseware") # pylint: disable=C0103
+log = logging.getLogger("mitx.courseware")
# Generate this many different variants of problems with rerandomize=per_student
@@ -51,9 +51,6 @@ class Randomization(String):
Define a field to store how to randomize a problem.
"""
def from_json(self, value):
- """
- For backward compatability?
- """
if value in ("", "true"):
return "always"
elif value == "false":
@@ -865,8 +862,8 @@ class CapaModule(CapaFields, XModule):
except Exception as err:
if self.system.DEBUG:
- msg = "Error checking problem: " + err.message
- msg += '\nTraceback:\n' + traceback.format_exc()
+ msg = u"Error checking problem: {}".format(err.message)
+ msg += u'\nTraceback:\n{}'.format(traceback.format_exc())
return {'success': msg}
raise
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 81df686015..c6ffd32e89 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -505,9 +505,10 @@ class CapaModuleTest(unittest.TestCase):
def test_check_problem_error(self):
# Try each exception that capa_module should handle
- for exception_class in [StudentInputError,
- LoncapaProblemError,
- ResponseError]:
+ exception_classes = [StudentInputError,
+ LoncapaProblemError,
+ ResponseError]
+ for exception_class in exception_classes:
# Create the module
module = CapaFactory.create(attempts=1)
@@ -532,9 +533,10 @@ class CapaModuleTest(unittest.TestCase):
def test_check_problem_error_nonascii(self):
# Try each exception that capa_module should handle
- for exception_class in [StudentInputError,
- LoncapaProblemError,
- ResponseError]:
+ exception_classes = [StudentInputError,
+ LoncapaProblemError,
+ ResponseError]
+ for exception_class in exception_classes:
# Create the module
module = CapaFactory.create(attempts=1)
From 9bfddd4891281dfeeb37b2d596c41627908813e2 Mon Sep 17 00:00:00 2001
From: Jean Manuel Nater
Date: Fri, 21 Jun 2013 14:05:57 -0400
Subject: [PATCH 016/161] 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 58fe6d4e8367c570648068d3307cc3105a88edf5 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Fri, 21 Jun 2013 16:17:33 -0400
Subject: [PATCH 017/161] Cleaned up import and comment
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index b0cbcee032..6d2055d459 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -45,7 +45,7 @@ import xmodule.contentstore.django
import datetime
from pytz import UTC
from uuid import uuid4
-import pymongo
+from pymongo import MongoClient
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
@@ -62,7 +62,6 @@ class MongoCollectionFindWrapper(object):
return self.original(query, *args, **kwargs)
-#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
@@ -91,7 +90,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
def tearDown(self):
- m = pymongo.MongoClient()
+ m = MongoClient()
m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
xmodule.contentstore.django._CONTENTSTORE.clear()
@@ -858,7 +857,7 @@ class ContentStoreTest(ModuleStoreTestCase):
}
def tearDown(self):
- m = pymongo.MongoClient()
+ m = MongoClient()
m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
xmodule.contentstore.django._CONTENTSTORE.clear()
From 5e6de488abaa45f765b5aef48a1b36851a673be1 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Fri, 21 Jun 2013 16:28:32 -0400
Subject: [PATCH 018/161] Fixed pylint/pep8 violations
---
.../contentstore/tests/test_contentstore.py | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 6d2055d459..514b631521 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -90,8 +90,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
def tearDown(self):
- m = MongoClient()
- m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
+ mongo = MongoClient()
+ mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
xmodule.contentstore.django._CONTENTSTORE.clear()
def check_components_on_page(self, component_types, expected_types):
@@ -414,7 +414,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertGreater(len(all_assets), 0)
# make sure we have some thumbnails in our contentstore
- all_thumbnails = content_store.get_all_content_thumbnails_for_course(course_location)
+ content_store.get_all_content_thumbnails_for_course(course_location)
#
# cdodge: temporarily comment out assertion on thumbnails because many environments
@@ -543,7 +543,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
all_assets = trash_store.get_all_content_for_course(course_location)
self.assertEqual(len(all_assets), 0)
-
all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
self.assertEqual(len(all_thumbnails), 0)
@@ -608,7 +607,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertRaises(InvalidVersionError, draft_store.unpublish, location)
-
def test_bad_contentstore_request(self):
resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png')
self.assertEqual(resp.status_code, 400)
@@ -857,8 +855,8 @@ class ContentStoreTest(ModuleStoreTestCase):
}
def tearDown(self):
- m = MongoClient()
- m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
+ mongo = MongoClient()
+ mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
xmodule.contentstore.django._CONTENTSTORE.clear()
def test_create_course(self):
From 3f9a72e6ce805a63d091cc387b44021d079d46c4 Mon Sep 17 00:00:00 2001
From: JonahStanley
Date: Fri, 21 Jun 2013 16:32:13 -0400
Subject: [PATCH 019/161] Consolidated imports
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 514b631521..66fead562e 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -23,7 +23,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore
-from xmodule.contentstore.django import contentstore
+from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
@@ -41,7 +41,6 @@ from xmodule.exceptions import NotFoundError
from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.exceptions import InvalidVersionError
-import xmodule.contentstore.django
import datetime
from pytz import UTC
from uuid import uuid4
@@ -92,7 +91,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def tearDown(self):
mongo = MongoClient()
mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
- xmodule.contentstore.django._CONTENTSTORE.clear()
+ _CONTENTSTORE.clear()
def check_components_on_page(self, component_types, expected_types):
"""
@@ -857,7 +856,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def tearDown(self):
mongo = MongoClient()
mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
- xmodule.contentstore.django._CONTENTSTORE.clear()
+ _CONTENTSTORE.clear()
def test_create_course(self):
"""Test new course creation - happy path"""
From e045860cb652686f8ab5bcaff659a636db6f4d32 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 20 Jun 2013 20:36:51 -0400
Subject: [PATCH 020/161] Pylint complains if you use string, even if you use
it for what its still meant to be used for.
---
common/djangoapps/external_auth/views.py | 2 +-
common/djangoapps/student/views.py | 2 +-
common/lib/symmath/symmath/formula.py | 2 +-
lms/djangoapps/django_comment_client/tests.py | 2 +-
lms/djangoapps/lms_migration/management/commands/create_user.py | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py
index 93ab70debb..06709eff9e 100644
--- a/common/djangoapps/external_auth/views.py
+++ b/common/djangoapps/external_auth/views.py
@@ -3,7 +3,7 @@ import json
import logging
import random
import re
-import string
+import string # pylint: disable=W0402
import fnmatch
from textwrap import dedent
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index e065333409..6b9c9104c5 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -4,7 +4,7 @@ import json
import logging
import random
import re
-import string
+import string # pylint: disable=W0402
import urllib
import uuid
import time
diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py
index ca4e20ace3..d5b97a2550 100644
--- a/common/lib/symmath/symmath/formula.py
+++ b/common/lib/symmath/symmath/formula.py
@@ -10,7 +10,7 @@
# Provides sympy representation.
import os
-import string
+import string # pylint: disable=W0402
import re
import logging
import operator
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index 8fd8ed7e2b..8c6a48d8c1 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -1,4 +1,4 @@
-import string
+import string # pylint: disable=W0402
import random
from django.contrib.auth.models import User
diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py
index 87abf4f73a..5d96d96a8a 100644
--- a/lms/djangoapps/lms_migration/management/commands/create_user.py
+++ b/lms/djangoapps/lms_migration/management/commands/create_user.py
@@ -6,7 +6,7 @@
import os
import sys
-import string
+import string # pylint: disable=W0402
import datetime
from getpass import getpass
import json
From df4b512b6f2651fea6894d4f1ab9e923eaec2bd4 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 21 Jun 2013 10:42:17 -0400
Subject: [PATCH 021/161] Change wildcard imports into specific imports
---
common/djangoapps/heartbeat/urls.py | 2 +-
common/djangoapps/student/admin.py | 4 ++--
common/djangoapps/track/admin.py | 2 +-
lms/djangoapps/courseware/admin.py | 2 +-
lms/djangoapps/django_comment_client/helpers.py | 2 +-
.../instructor/management/commands/compute_grades.py | 2 +-
lms/djangoapps/psychometrics/admin.py | 2 +-
.../psychometrics/management/commands/init_psychometrics.py | 6 +++---
lms/djangoapps/psychometrics/psychoanalyze.py | 5 +++--
lms/lib/comment_client/comment.py | 4 ++--
lms/lib/comment_client/comment_client.py | 2 +-
lms/lib/comment_client/commentable.py | 2 --
lms/lib/comment_client/thread.py | 3 ++-
lms/lib/comment_client/user.py | 2 +-
14 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/common/djangoapps/heartbeat/urls.py b/common/djangoapps/heartbeat/urls.py
index 3f45a95dd2..6a0be757c9 100644
--- a/common/djangoapps/heartbeat/urls.py
+++ b/common/djangoapps/heartbeat/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import *
+from django.conf.urls import url, patterns
urlpatterns = patterns('', # nopep8
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py
index 64fe844801..4d6976d7d4 100644
--- a/common/djangoapps/student/admin.py
+++ b/common/djangoapps/student/admin.py
@@ -2,9 +2,9 @@
django admin pages for courseware model
'''
-from student.models import *
+from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
+from student.models import CourseEnrollment, Registration, PendingNameChange
from django.contrib import admin
-from django.contrib.auth.models import User
admin.site.register(UserProfile)
diff --git a/common/djangoapps/track/admin.py b/common/djangoapps/track/admin.py
index 1f19c59a93..d75f206846 100644
--- a/common/djangoapps/track/admin.py
+++ b/common/djangoapps/track/admin.py
@@ -2,7 +2,7 @@
django admin pages for courseware model
'''
-from track.models import *
+from track.models import TrackingLog
from django.contrib import admin
admin.site.register(TrackingLog)
diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py
index 9ef4c1de20..743d1fed52 100644
--- a/lms/djangoapps/courseware/admin.py
+++ b/lms/djangoapps/courseware/admin.py
@@ -2,7 +2,7 @@
django admin pages for courseware model
'''
-from courseware.models import *
+from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog
from django.contrib import admin
from django.contrib.auth.models import User
diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py
index a8a51ad95c..1310c4e0c1 100644
--- a/lms/djangoapps/django_comment_client/helpers.py
+++ b/lms/djangoapps/django_comment_client/helpers.py
@@ -2,7 +2,7 @@ from django.conf import settings
from .mustache_helpers import mustache_helpers
from functools import partial
-from .utils import *
+from .utils import extend_content, merge_dict, render_mustache
import django_comment_client.settings as cc_settings
import pystache_custom as pystache
diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py
index 4518450e39..d1c66d51d2 100644
--- a/lms/djangoapps/instructor/management/commands/compute_grades.py
+++ b/lms/djangoapps/instructor/management/commands/compute_grades.py
@@ -3,7 +3,7 @@
# django management command: dump grades to csv files
# for use by batch processes
-from instructor.offline_gradecalc import *
+from instructor.offline_gradecalc import offline_grade_calculation
from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore
diff --git a/lms/djangoapps/psychometrics/admin.py b/lms/djangoapps/psychometrics/admin.py
index ff1a14d722..b7c04b5069 100644
--- a/lms/djangoapps/psychometrics/admin.py
+++ b/lms/djangoapps/psychometrics/admin.py
@@ -2,7 +2,7 @@
django admin pages for courseware model
'''
-from psychometrics.models import *
+from psychometrics.models import PsychometricData
from django.contrib import admin
admin.site.register(PsychometricData)
diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py
index 87e62f4a2c..f9cfbd28f5 100644
--- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py
+++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py
@@ -4,9 +4,9 @@
import json
-from courseware.models import *
-from track.models import *
-from psychometrics.models import *
+from courseware.models import StudentModule
+from track.models import TrackingLog
+from psychometrics.models import PsychometricData
from xmodule.modulestore import Location
from django.conf import settings
diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py
index ab9a5e6242..c6e66445a4 100644
--- a/lms/djangoapps/psychometrics/psychoanalyze.py
+++ b/lms/djangoapps/psychometrics/psychoanalyze.py
@@ -14,7 +14,8 @@ from scipy.optimize import curve_fit
from django.conf import settings
from django.db.models import Sum, Max
-from psychometrics.models import *
+from psychometrics.models import PsychometricData
+from courseware.models import StudentModule
from pytz import UTC
log = logging.getLogger("mitx.psychometrics")
@@ -303,7 +304,7 @@ def generate_plots_for_problem(problem):
def make_psychometrics_data_update_handler(course_id, user, module_state_key):
"""
Construct and return a procedure which may be called to update
- the PsychometricsData instance for the given StudentModule instance.
+ the PsychometricData instance for the given StudentModule instance.
"""
sm, status = StudentModule.objects.get_or_create(
course_id=course_id,
diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py
index fb5a4ad0c3..fd68d5cdeb 100644
--- a/lms/lib/comment_client/comment.py
+++ b/lms/lib/comment_client/comment.py
@@ -1,6 +1,6 @@
-from .utils import *
+from .utils import CommentClientError, perform_request
-from .thread import Thread
+from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread
import models
import settings
diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py
index d91c5ea47f..4f660533f1 100644
--- a/lms/lib/comment_client/comment_client.py
+++ b/lms/lib/comment_client/comment_client.py
@@ -5,7 +5,7 @@ from .thread import Thread
from .user import User
from .commentable import Commentable
-from .utils import *
+from .utils import perform_request
import settings
diff --git a/lms/lib/comment_client/commentable.py b/lms/lib/comment_client/commentable.py
index 111809f8f0..05efd70e50 100644
--- a/lms/lib/comment_client/commentable.py
+++ b/lms/lib/comment_client/commentable.py
@@ -1,5 +1,3 @@
-from .utils import *
-
import models
import settings
diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py
index 0b0be576b8..00d5f01814 100644
--- a/lms/lib/comment_client/thread.py
+++ b/lms/lib/comment_client/thread.py
@@ -1,4 +1,5 @@
-from .utils import *
+from .utils import merge_dict, strip_blank, strip_none, extract, perform_request
+from .utils import CommentClientError
import models
import settings
diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py
index a9e47fe6aa..2370052d90 100644
--- a/lms/lib/comment_client/user.py
+++ b/lms/lib/comment_client/user.py
@@ -1,4 +1,4 @@
-from .utils import *
+from .utils import merge_dict, perform_request, CommentClientError
import models
import settings
From 75b390124f402b3a1519ee6a9b40e3827c155f2d Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 21 Jun 2013 10:42:49 -0400
Subject: [PATCH 022/161] Tweaks to our pylintrc rules.
---
pylintrc | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pylintrc b/pylintrc
index af958e4af4..dea0f240c6 100644
--- a/pylintrc
+++ b/pylintrc
@@ -41,6 +41,10 @@ disable=
# W0142: Used * or ** magic
I0011,C0301,W0141,W0142,
+# Django makes classes that trigger these
+# W0232: Class has no __init__ method
+ W0232,
+
# Might use these when the code is in better shape
# C0302: Too many lines in module
# R0201: Method could be a function
From 57909ce1aaba363645eef900fd1760d7aa276327 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 21 Jun 2013 10:55:36 -0400
Subject: [PATCH 023/161] Fix all W0602, global used but no assignment done.
---
.../djangoapps/student/management/commands/massemailtxt.py | 1 -
common/lib/xmodule/xmodule/contentstore/django.py | 2 --
common/lib/xmodule/xmodule/modulestore/django.py | 2 --
lms/djangoapps/django_comment_client/utils.py | 6 ------
4 files changed, 11 deletions(-)
diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py
index fec354e974..ae25430a85 100644
--- a/common/djangoapps/student/management/commands/massemailtxt.py
+++ b/common/djangoapps/student/management/commands/massemailtxt.py
@@ -37,7 +37,6 @@ rate -- messages per second
self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n')
def handle(self, *args, **options):
- global log_file
(user_file, message_base, logfilename, ratestr) = args
users = [u.strip() for u in open(user_file).readlines()]
diff --git a/common/lib/xmodule/xmodule/contentstore/django.py b/common/lib/xmodule/xmodule/contentstore/django.py
index f163348cc8..25a5d7912f 100644
--- a/common/lib/xmodule/xmodule/contentstore/django.py
+++ b/common/lib/xmodule/xmodule/contentstore/django.py
@@ -18,8 +18,6 @@ def load_function(path):
def contentstore(name='default'):
- global _CONTENTSTORE
-
if name not in _CONTENTSTORE:
class_ = load_function(settings.CONTENTSTORE['ENGINE'])
options = {}
diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py
index a2e2a4a5a5..c98e6cadef 100644
--- a/common/lib/xmodule/xmodule/modulestore/django.py
+++ b/common/lib/xmodule/xmodule/modulestore/django.py
@@ -26,8 +26,6 @@ def load_function(path):
def modulestore(name='default'):
- global _MODULESTORES
-
if name not in _MODULESTORES:
class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 496c834950..6668826b67 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -73,21 +73,17 @@ def get_discussion_id_map(course):
"""
return a dict of the form {category: modules}
"""
- global _DISCUSSIONINFO
initialize_discussion_info(course)
return _DISCUSSIONINFO[course.id]['id_map']
def get_discussion_title(course, discussion_id):
- global _DISCUSSIONINFO
initialize_discussion_info(course)
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
return title
def get_discussion_category_map(course):
-
- global _DISCUSSIONINFO
initialize_discussion_info(course)
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
@@ -141,8 +137,6 @@ def sort_map_entries(category_map):
def initialize_discussion_info(course):
- global _DISCUSSIONINFO
-
course_id = course.id
discussion_id_map = {}
From 45815e2d03f3a53defbf629bda73c653935f28db Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 21 Jun 2013 14:03:56 -0400
Subject: [PATCH 024/161] Remove obsolete file comment_client/legacy.py
---
lms/lib/comment_client/legacy.py | 226 -------------------------------
1 file changed, 226 deletions(-)
delete mode 100644 lms/lib/comment_client/legacy.py
diff --git a/lms/lib/comment_client/legacy.py b/lms/lib/comment_client/legacy.py
deleted file mode 100644
index de7ce201ce..0000000000
--- a/lms/lib/comment_client/legacy.py
+++ /dev/null
@@ -1,226 +0,0 @@
-def delete_threads(commentable_id, *args, **kwargs):
- return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
-
-
-def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs):
- default_params = {'page': 1, 'per_page': 20, 'recursive': recursive}
- attributes = dict(default_params.items() + query_params.items())
- response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs)
- return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
-
-
-def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
- default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive}
- attributes = dict(default_params.items() + query_params.items())
- response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
- return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
-
-
-def search_similar_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
- default_params = {'course_id': course_id, 'recursive': recursive}
- attributes = dict(default_params.items() + query_params.items())
- return _perform_request('get', _url_for_search_similar_threads(), attributes, *args, **kwargs)
-
-
-def search_recent_active_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
- default_params = {'course_id': course_id, 'recursive': recursive}
- attributes = dict(default_params.items() + query_params.items())
- return _perform_request('get', _url_for_search_recent_active_threads(), attributes, *args, **kwargs)
-
-
-def search_trending_tags(course_id, query_params={}, *args, **kwargs):
- default_params = {'course_id': course_id}
- attributes = dict(default_params.items() + query_params.items())
- return _perform_request('get', _url_for_search_trending_tags(), attributes, *args, **kwargs)
-
-
-def create_user(attributes, *args, **kwargs):
- return _perform_request('post', _url_for_users(), attributes, *args, **kwargs)
-
-
-def update_user(user_id, attributes, *args, **kwargs):
- return _perform_request('put', _url_for_user(user_id), attributes, *args, **kwargs)
-
-
-def get_threads_tags(*args, **kwargs):
- return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
-
-
-def tags_autocomplete(value, *args, **kwargs):
- return _perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs)
-
-
-def create_thread(commentable_id, attributes, *args, **kwargs):
- return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs)
-
-
-def get_thread(thread_id, recursive=False, *args, **kwargs):
- return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs)
-
-
-def update_thread(thread_id, attributes, *args, **kwargs):
- return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs)
-
-
-def create_comment(thread_id, attributes, *args, **kwargs):
- return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs)
-
-
-def delete_thread(thread_id, *args, **kwargs):
- return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs)
-
-
-def get_comment(comment_id, recursive=False, *args, **kwargs):
- return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs)
-
-
-def update_comment(comment_id, attributes, *args, **kwargs):
- return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs)
-
-
-def create_sub_comment(comment_id, attributes, *args, **kwargs):
- return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs)
-
-
-def delete_comment(comment_id, *args, **kwargs):
- return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs)
-
-
-def vote_for_comment(comment_id, user_id, value, *args, **kwargs):
- return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
-
-
-def undo_vote_for_comment(comment_id, user_id, *args, **kwargs):
- return _perform_request('delete', _url_for_vote_comment(comment_id), {'user_id': user_id}, *args, **kwargs)
-
-
-def vote_for_thread(thread_id, user_id, value, *args, **kwargs):
- return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
-
-
-def undo_vote_for_thread(thread_id, user_id, *args, **kwargs):
- return _perform_request('delete', _url_for_vote_thread(thread_id), {'user_id': user_id}, *args, **kwargs)
-
-
-def get_notifications(user_id, *args, **kwargs):
- return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs)
-
-
-def get_user_info(user_id, complete=True, *args, **kwargs):
- return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs)
-
-
-def subscribe(user_id, subscription_detail, *args, **kwargs):
- return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
-
-
-def subscribe_user(user_id, followed_user_id, *args, **kwargs):
- return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
-
-follow = subscribe_user
-
-
-def subscribe_thread(user_id, thread_id, *args, **kwargs):
- return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
-
-
-def subscribe_commentable(user_id, commentable_id, *args, **kwargs):
- return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
-
-
-def unsubscribe(user_id, subscription_detail, *args, **kwargs):
- return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
-
-
-def unsubscribe_user(user_id, followed_user_id, *args, **kwargs):
- return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
-
-unfollow = unsubscribe_user
-
-
-def unsubscribe_thread(user_id, thread_id, *args, **kwargs):
- return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
-
-
-def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
- return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
-
-
-def _perform_request(method, url, data_or_params=None, *args, **kwargs):
- if method in ['post', 'put', 'patch']:
- response = requests.request(method, url, data=data_or_params)
- else:
- response = requests.request(method, url, params=data_or_params)
- if 200 < response.status_code < 500:
- raise CommentClientError(response.text)
- elif response.status_code == 500:
- raise CommentClientUnknownError(response.text)
- else:
- if kwargs.get("raw", False):
- return response.text
- else:
- return json.loads(response.text)
-
-
-def _url_for_threads(commentable_id):
- return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id)
-
-
-def _url_for_thread(thread_id):
- return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id)
-
-
-def _url_for_thread_comments(thread_id):
- return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id)
-
-
-def _url_for_comment(comment_id):
- return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id)
-
-
-def _url_for_vote_comment(comment_id):
- return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id)
-
-
-def _url_for_vote_thread(thread_id):
- return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id)
-
-
-def _url_for_notifications(user_id):
- return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id)
-
-
-def _url_for_subscription(user_id):
- return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id)
-
-
-def _url_for_user(user_id):
- return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id)
-
-
-def _url_for_search_threads():
- return "{prefix}/search/threads".format(prefix=PREFIX)
-
-
-def _url_for_search_similar_threads():
- return "{prefix}/search/threads/more_like_this".format(prefix=PREFIX)
-
-
-def _url_for_search_recent_active_threads():
- return "{prefix}/search/threads/recent_active".format(prefix=PREFIX)
-
-
-def _url_for_search_trending_tags():
- return "{prefix}/search/tags/trending".format(prefix=PREFIX)
-
-
-def _url_for_threads_tags():
- return "{prefix}/threads/tags".format(prefix=PREFIX)
-
-
-def _url_for_threads_tags_autocomplete():
- return "{prefix}/threads/tags/autocomplete".format(prefix=PREFIX)
-
-
-def _url_for_users():
- return "{prefix}/users".format(prefix=PREFIX)
From 5a5d425eb348e2c646037879d54c997c00b4bf6f Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Fri, 21 Jun 2013 14:41:16 -0400
Subject: [PATCH 025/161] Files that may not exist need F0401 suppressed during
import.
---
cms/envs/dev.py | 2 +-
lms/envs/dev.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 07630bdf31..2dcb3640ca 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -181,6 +181,6 @@ if SEGMENT_IO_KEY:
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
- from .private import *
+ from .private import * # pylint: disable=F0401
except ImportError:
pass
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index b1519b77bc..813f9cf32c 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY:
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
- from .private import *
+ from .private import * # pylint: disable=F0401
except ImportError:
pass
From fa9a8f4af09a27bd88aeea33a81ec0f5086d9363 Mon Sep 17 00:00:00 2001
From: Julian Arni
Date: Fri, 21 Jun 2013 18:00:30 -0400
Subject: [PATCH 026/161] Greater dir naming flexibility.
Accepts any dirname for the edx-platform repo. Allows the script to be
called from any directory, not just $BASE/edx-platform.
---
scripts/create-dev-env.sh | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh
index edb0bcdcae..0816b72d21 100755
--- a/scripts/create-dev-env.sh
+++ b/scripts/create-dev-env.sh
@@ -98,19 +98,23 @@ clone_repos() {
set_base_default() { # if PROJECT_HOME not set
# 2 possibilities: this is from cloned repo, or not
- # this script is in "./scripts" if a git clone
- this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd)
- if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then
- # set BASE one-up from this_repo;
- echo "${this_repo%/*}"
+
+ # See if remote's url is named edx-platform (this works for forks too, but
+ # not if the name was changed).
+ cd "$( dirname "${BASH_SOURCE[0]}" )"
+ this_repo=$(basename $(git ls-remote --get-url 2>/dev/null) 2>/dev/null) ||
+ echo -n ""
+
+ if [[ "x$this_repo" = "xedx-platform.git" ]]; then
+ # We are in the edx repo and already have git installed. Let git do the
+ # work of finding base dir:
+ echo "$(dirname $(git rev-parse --show-toplevel))"
else
echo "$HOME/edx_all"
fi
}
-
-
### START
PROG=${0##*/}
From 2eefa494b145610eb8b9a2037ee3be7aa0daba51 Mon Sep 17 00:00:00 2001
From: Jason Bau
Date: Fri, 21 Jun 2013 23:46:56 -0700
Subject: [PATCH 027/161] Width of labels for multiple-choice capa problems =
width of text
Stanford got some feedback from our students/faculty that students
were making accidental clicks on radio-button capa questions.
They would click way to the right of the label text, but it would
still select the corresponding input, which caused some students
to make unintentional changes to their answers. This was because
labels for these inputs were display:block and width:100%
Changing these labels to float:left clear:both should fix it.
---
lms/static/sass/course/base/_base.scss | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss
index 6d87b7f554..a1c948d4f5 100644
--- a/lms/static/sass/course/base/_base.scss
+++ b/lms/static/sass/course/base/_base.scss
@@ -46,6 +46,13 @@ form {
}
}
+form.choicegroup {
+ label {
+ clear: both;
+ float: left;
+ }
+}
+
textarea,
input[type="text"],
input[type="email"],
From 85b4a4ccab37e14e6f0543f8b7165e667c0768ef Mon Sep 17 00:00:00 2001
From: Alexander Kryklia
Date: Sat, 22 Jun 2013 16:13:40 +0300
Subject: [PATCH 028/161] removes choiceresponse wiping after clicking Show
Answer
---
common/lib/xmodule/xmodule/js/src/capa/display.coffee | 3 ---
1 file changed, 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 987d20b65a..f773fc81c4 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -364,8 +364,6 @@ class @Problem
choicegroup: (element, display, answers) =>
element = $(element)
- element.find('input').attr('disabled', 'disabled')
-
input_id = element.attr('id').replace(/inputtype_/,'')
answer = answers[input_id]
for choice in answer
@@ -379,7 +377,6 @@ class @Problem
inputtypeHideAnswerMethods:
choicegroup: (element, display) =>
element = $(element)
- element.find('input').attr('disabled', null)
element.find('label').removeClass('choicegroup_correct')
javascriptinput: (element, display) =>
From 4a98e2eda75b1a8b036e4f3f5e035c5049aab776 Mon Sep 17 00:00:00 2001
From: Jason Bau
Date: Wed, 5 Jun 2013 23:14:18 -0700
Subject: [PATCH 029/161] Moves user activation away from just clicking on
reset password
To following the link in the password reset email
---
common/djangoapps/student/forms.py | 72 +++++++++++++++++++
common/djangoapps/student/views.py | 31 ++++----
.../registration/password_reset_email.html | 2 +-
lms/urls.py | 2 +-
4 files changed, 93 insertions(+), 14 deletions(-)
create mode 100644 common/djangoapps/student/forms.py
diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py
new file mode 100644
index 0000000000..75c89e0a26
--- /dev/null
+++ b/common/djangoapps/student/forms.py
@@ -0,0 +1,72 @@
+from django import forms
+from django.utils.translation import ugettext, ugettext_lazy as _
+from django.template import loader
+from django.contrib.auth.models import User
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher
+from django.contrib.auth.tokens import default_token_generator
+from django.contrib.sites.models import get_current_site
+from django.utils.http import int_to_base36
+
+
+
+# This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm
+# I think copy-and-paste here is somewhat better than subclassing and
+# just changing the definition of clean_email, because it's less
+# likely to be broken by incompatibility with a new django version.
+# (If this form is good enough now, a snapshot of it ought to last a while)
+
+class PasswordResetFormNoActive(forms.Form):
+ error_messages = {
+ 'unknown': _("That e-mail address doesn't have an associated "
+ "user account. Are you sure you've registered?"),
+ 'unusable': _("The user account associated with this e-mail "
+ "address cannot reset the password."),
+ }
+ email = forms.EmailField(label=_("E-mail"), max_length=75)
+
+ def clean_email(self):
+ """
+ Validates that an active user exists with the given email address.
+ """
+ email = self.cleaned_data["email"]
+ #The line below contains the only change, removing is_active=True
+ self.users_cache = User.objects.filter(email__iexact=email)
+ if not len(self.users_cache):
+ raise forms.ValidationError(self.error_messages['unknown'])
+ if any((user.password == UNUSABLE_PASSWORD)
+ for user in self.users_cache):
+ raise forms.ValidationError(self.error_messages['unusable'])
+ return email
+
+ def save(self, domain_override=None,
+ subject_template_name='registration/password_reset_subject.txt',
+ email_template_name='registration/password_reset_email.html',
+ use_https=False, token_generator=default_token_generator,
+ from_email=None, request=None):
+ """
+ Generates a one-use only link for resetting password and sends to the
+ user.
+ """
+ from django.core.mail import send_mail
+ for user in self.users_cache:
+ if not domain_override:
+ current_site = get_current_site(request)
+ site_name = current_site.name
+ domain = current_site.domain
+ else:
+ site_name = domain = domain_override
+ c = {
+ 'email': user.email,
+ 'domain': domain,
+ 'site_name': site_name,
+ 'uid': int_to_base36(user.id),
+ 'user': user,
+ 'token': token_generator.make_token(user),
+ 'protocol': use_https and 'https' or 'http',
+ }
+ subject = loader.render_to_string(subject_template_name, c)
+ # Email subject *must not* contain newlines
+ subject = ''.join(subject.splitlines())
+ email = loader.render_to_string(email_template_name, c)
+ send_mail(subject, email, from_email, [user.email])
+
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index e065333409..50f6d90368 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -11,9 +11,9 @@ import time
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
-from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
+from django.contrib.auth.views import password_reset_confirm
from django.core.cache import cache
from django.core.context_processors import csrf
from django.core.mail import send_mail
@@ -24,6 +24,7 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbid
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date
+from django.utils.http import base36_to_int
from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup
@@ -34,6 +35,8 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
CourseEnrollment, unique_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed)
+from student.forms import PasswordResetFormNoActive
+
from certificates.models import CertificateStatuses, certificate_status_for_student
from xmodule.course_module import CourseDescriptor
@@ -962,17 +965,7 @@ def password_reset(request):
if request.method != "POST":
raise Http404
- # By default, Django doesn't allow Users with is_active = False to reset their passwords,
- # but this bites people who signed up a long time ago, never activated, and forgot their
- # password. So for their sake, we'll auto-activate a user for whom password_reset is called.
- try:
- user = User.objects.get(email=request.POST['email'])
- user.is_active = True
- user.save()
- except:
- log.exception("Tried to auto-activate user to enable password reset, but failed.")
-
- form = PasswordResetForm(request.POST)
+ form = PasswordResetFormNoActive(request.POST)
if form.is_valid():
form.save(use_https=request.is_secure(),
from_email=settings.DEFAULT_FROM_EMAIL,
@@ -984,6 +977,20 @@ def password_reset(request):
return HttpResponse(json.dumps({'success': False,
'error': 'Invalid e-mail'}))
+def password_reset_confirm_wrapper(request, uidb36=None, token=None):
+ ''' A wrapper around django.contrib.auth.views.password_reset_confirm.
+ Needed because we want to set the user as active at this step.
+ '''
+ #cribbed from django.contrib.auth.views.password_reset_confirm
+ try:
+ uid_int = base36_to_int(uidb36)
+ user = User.objects.get(id=uid_int)
+ user.is_active = True
+ user.save()
+ except (ValueError, User.DoesNotExist):
+ pass
+ return password_reset_confirm(request, uidb36=uidb36, token=token)
+
def reactivation_email_for_user(user):
try:
diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html
index bf6c3e0891..68073d9ddd 100644
--- a/lms/templates/registration/password_reset_email.html
+++ b/lms/templates/registration/password_reset_email.html
@@ -3,7 +3,7 @@
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
-https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
+https://{{domain}}{% url 'student.views.password_reset_confirm_wrapper' uidb36=uid token=token %}
{% endblock %}
If you didn't request this change, you can disregard this email - we have not yet reset your password.
diff --git a/lms/urls.py b/lms/urls.py
index 52ce539f73..50ce35cde0 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -51,7 +51,7 @@ urlpatterns = ('', # nopep8
url(r'^password_change_done/$', django.contrib.auth.views.password_change_done,
name='auth_password_change_done'),
url(r'^password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/$',
- django.contrib.auth.views.password_reset_confirm,
+ 'student.views.password_reset_confirm_wrapper',
name='auth_password_reset_confirm'),
url(r'^password_reset_complete/$', django.contrib.auth.views.password_reset_complete,
name='auth_password_reset_complete'),
From 72e08456a8dcebd61e8a3476504c25209546f780 Mon Sep 17 00:00:00 2001
From: Peter Fogg
Date: Fri, 21 Jun 2013 16:50:32 -0400
Subject: [PATCH 030/161] Refactor Advanced Settings page to use Backbone
notifications.
---
cms/static/js/views/settings/advanced_view.js | 91 ++++++++++---------
cms/templates/base.html | 6 --
cms/templates/settings_advanced.html | 57 ------------
3 files changed, 46 insertions(+), 108 deletions(-)
diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js
index 863393d341..69a2c9f622 100644
--- a/cms/static/js/views/settings/advanced_view.js
+++ b/cms/static/js/views/settings/advanced_view.js
@@ -20,9 +20,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
self.render();
}
);
- // because these are outside of this.$el, they can't be in the event hash
- $('.save-button').on('click', this, this.saveView);
- $('.cancel-button').on('click', this, this.revertView);
this.listenTo(this.model, 'invalid', this.handleValidationError);
},
render: function() {
@@ -45,7 +42,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
var policyValues = listEle$.find('.json');
_.each(policyValues, this.attachJSONEditor, this);
- this.showMessage();
return this;
},
attachJSONEditor : function (textarea) {
@@ -61,7 +57,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) {
// this event's being called even when there's no change :-(
- if (instance.getValue() !== oldValue) self.showSaveCancelButtons();
+ if (instance.getValue() !== oldValue && !self.notificationBarShowing) {
+ self.showNotificationBar();
+ }
},
onFocus : function(mirror) {
$(textarea).parent().children('label').addClass("is-focused");
@@ -99,59 +97,62 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
});
},
- showMessage: function (type) {
- $(".wrapper-alert").removeClass("is-shown");
- if (type) {
- if (type === this.error_saving) {
- $(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false');
- }
- else if (type === this.successful_changes) {
- $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
- this.hideSaveCancelButtons();
- }
- }
- else {
- // This is the case of the page first rendering, or when Cancel is pressed.
- this.hideSaveCancelButtons();
- }
+ showNotificationBar: function() {
+ var self = this;
+ var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.")
+ var confirm = new CMS.Views.Notification.Warning({
+ title: gettext("You've Made Some Changes"),
+ message: message,
+ actions: {
+ primary: {
+ "text": gettext("Save Changes"),
+ "class": "action-save",
+ "click": function() {
+ self.saveView();
+ confirm.hide();
+ self.notificationBarShowing = false;
+ }
+ },
+ secondary: [{
+ "text": gettext("Cancel"),
+ "class": "action-cancel",
+ "click": function() {
+ self.revertView();
+ confirm.hide();
+ self.notificationBarShowing = false;
+ }
+ }],
+ }});
+ this.notificationBarShowing = true;
+ confirm.show();
},
- showSaveCancelButtons: function(event) {
- if (!this.notificationBarShowing) {
- this.$el.find(".message-status").removeClass("is-shown");
- $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false');
- this.notificationBarShowing = true;
- }
- },
- hideSaveCancelButtons: function() {
- if (this.notificationBarShowing) {
- $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
- this.notificationBarShowing = false;
- }
- },
- saveView : function(event) {
- window.CmsUtils.smoothScrollTop(event);
+ saveView : function() {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
// check for dupes
- var self = event.data;
- self.model.save({},
+ var self = this;
+ this.model.save({},
{
success : function() {
self.render();
- self.showMessage(self.successful_changes);
+ var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.");
+ var saving = new CMS.Views.Alert.Confirmation({
+ title: gettext("Your policy changes have been saved."),
+ message: message,
+ closeIcon: false
+ });
+ saving.show();
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
-
}
});
},
- revertView : function(event) {
- event.preventDefault();
- var self = event.data;
- self.model.deleteKeys = [];
- self.model.clear({silent : true});
- self.model.fetch({
+ revertView : function() {
+ var self = this;
+ this.model.deleteKeys = [];
+ this.model.clear({silent : true});
+ this.model.fetch({
success : function() { self.render(); },
reset: true
});
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 11e8d41496..695a97f1da 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -61,8 +61,6 @@
<%include file="widgets/header.html" />
- ## remove this block after advanced settings notification is rewritten
- <%block name="view_alerts">%block>
<%block name="content">%block>
@@ -74,13 +72,9 @@
<%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
- ## remove this block after advanced settings notification is rewritten
- <%block name="view_notifications">%block>
- ## remove this block after advanced settings notification is rewritten
- <%block name="view_prompts">%block>
<%block name="jsextra">%block>