Merge pull request #10725 from edx/rc/2015-11-24
Release Candidate rc/2015-11-24 for Dec 1 release
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
common/static/js/vendor
|
||||
lms/static/js/vendor
|
||||
**/vendor
|
||||
node_modules
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"spyOnEvent",
|
||||
|
||||
// Miscellaneous globals
|
||||
"JSON"
|
||||
"JSON",
|
||||
"gettext"
|
||||
]
|
||||
}
|
||||
|
||||
4
AUTHORS
4
AUTHORS
@@ -253,4 +253,6 @@ Justin Abrahms <abrahms@mit.edu>
|
||||
Arbab Nazar <arbab@edx.org>
|
||||
Douglas Hall <dhall@edx.org>
|
||||
Awais Jibran <awaisdar001@gmail.com>
|
||||
Muhammad Rehan <muhammadrehan69@gmail.com>
|
||||
Muhammad Rehan <muhammadrehan69@gmail.com>
|
||||
Shawn Milochik <shawn@milochik.com>
|
||||
Afeef Janjua <janjua.afeef@gmail.com>
|
||||
|
||||
@@ -640,3 +640,22 @@ class CourseAboutSearchIndexer(object):
|
||||
"Successfully added %s course to the course discovery index",
|
||||
course_id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_location_info(cls, normalized_structure_key):
|
||||
""" Builds location info dictionary """
|
||||
return {"course": unicode(normalized_structure_key), "org": normalized_structure_key.org}
|
||||
|
||||
@classmethod
|
||||
def remove_deleted_items(cls, structure_key):
|
||||
""" Remove item from Course About Search_index """
|
||||
searcher = SearchEngine.get_search_engine(cls.INDEX_NAME)
|
||||
if not searcher:
|
||||
return
|
||||
|
||||
response = searcher.search(
|
||||
doc_type=cls.DISCOVERY_DOCUMENT_TYPE,
|
||||
field_dictionary=cls._get_location_info(structure_key)
|
||||
)
|
||||
result_ids = [result["data"]["id"] for result in response["results"]]
|
||||
searcher.remove(cls.DISCOVERY_DOCUMENT_TYPE, result_ids)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches
|
||||
from common import type_in_codemirror, press_the_notification_button, get_codemirror_value
|
||||
|
||||
KEY_CSS = '.key h3.title'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_equal
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import os
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_in # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_in
|
||||
from django.conf import settings
|
||||
|
||||
from student.roles import CourseStaffRole, CourseInstructorRole, GlobalStaff
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_in, assert_equal
|
||||
|
||||
DISPLAY_NAME = "Display Name"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
from lettuce import world
|
||||
from nose.tools import assert_equal, assert_in # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_equal, assert_in
|
||||
from terrain.steps import reload_the_page
|
||||
from common import type_in_codemirror
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_false # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_false
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -6,7 +6,7 @@ from selenium.webdriver.common.keys import Keys
|
||||
from common import type_in_codemirror, upload_file
|
||||
from django.conf import settings
|
||||
|
||||
from nose.tools import assert_true, assert_false # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_false
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world, step
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from common import type_in_codemirror, get_codemirror_value
|
||||
from nose.tools import assert_in # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_in
|
||||
|
||||
|
||||
@step(u'I go to the course updates page')
|
||||
|
||||
@@ -6,7 +6,7 @@ from common import *
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import InvalidElementStateException
|
||||
from contentstore.utils import reverse_course_url
|
||||
from nose.tools import assert_in, assert_equal, assert_not_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_in, assert_equal, assert_not_equal
|
||||
|
||||
|
||||
@step(u'I am viewing the grading settings')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_in, assert_false, assert_true, assert_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_in, assert_false, assert_true, assert_equal
|
||||
from common import type_in_codemirror, get_codemirror_value
|
||||
|
||||
CODEMIRROR_SELECTOR_PREFIX = "$('iframe').contents().find"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal, assert_in # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_equal, assert_in
|
||||
|
||||
|
||||
CSS_FOR_TAB_ELEMENT = "li[data-tab-id='{0}'] input.toggle-checkbox"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import json
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal, assert_true # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_equal, assert_true
|
||||
from common import type_in_codemirror, open_new_course
|
||||
from advanced_settings import change_value, ADVANCED_MODULES_KEY
|
||||
from course_import import import_file
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_false # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_false
|
||||
|
||||
|
||||
@step('I fill in the registration form$')
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world, step
|
||||
from django.conf import settings
|
||||
|
||||
@@ -10,7 +10,7 @@ import random
|
||||
import os
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from nose.tools import assert_equal, assert_not_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_equal, assert_not_equal
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
ASSET_NAMES_CSS = 'td.name-col > span.title > a.filename'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import requests
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_equal, assert_in, assert_not_equal # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true, assert_equal, assert_in, assert_not_equal
|
||||
from terrain.steps import reload_the_page
|
||||
from django.conf import settings
|
||||
from common import upload_file, attach_file
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true
|
||||
from video_editor import RequestHandlerWithSessionId, success_upload_file
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ class Command(BaseCommand):
|
||||
help = '''
|
||||
Delete orphans from a MongoDB backed course. Takes two arguments:
|
||||
<course_id>: the course id of the course whose orphans you want to delete
|
||||
|commit|: optional argument. If not provided, will not run task.
|
||||
|--commit|: optional argument. If not provided, will dry run delete
|
||||
'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('course_id')
|
||||
parser.add_argument('--commit', action='store')
|
||||
parser.add_argument('--commit', action='store_true', help='Commit to deleting the orphans')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
@@ -24,20 +24,16 @@ class Command(BaseCommand):
|
||||
except InvalidKeyError:
|
||||
raise CommandError("Invalid course key.")
|
||||
|
||||
commit = False
|
||||
if options['commit']:
|
||||
commit = options['commit'] == 'commit'
|
||||
|
||||
if commit:
|
||||
print 'Deleting orphans from the course:'
|
||||
deleted_items = _delete_orphans(
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, commit
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit']
|
||||
)
|
||||
print "Success! Deleted the following orphans from the course:"
|
||||
print "\n".join(deleted_items)
|
||||
else:
|
||||
print 'Dry run. The following orphans would have been deleted from the course:'
|
||||
deleted_items = _delete_orphans(
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, commit
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit']
|
||||
)
|
||||
print "\n".join(deleted_items)
|
||||
|
||||
@@ -17,39 +17,37 @@ class Command(BaseCommand):
|
||||
help = '''
|
||||
Force publish a course. Takes two arguments:
|
||||
<course_id>: the course id of the course you want to publish forcefully
|
||||
commit: do the force publish
|
||||
--commit: do the force publish
|
||||
|
||||
If you do not specify 'commit', the command will print out what changes would be made.
|
||||
If you do not specify '--commit', the command will print out what changes would be made.
|
||||
'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('course_key', help="ID of the Course to force publish")
|
||||
parser.add_argument('--commit', action='store_true', help="Pull updated metadata from external IDPs")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Execute the command"""
|
||||
if len(args) not in {1, 2}:
|
||||
raise CommandError("force_publish requires 1 or more argument: <course_id> |commit|")
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(args[0])
|
||||
course_key = CourseKey.from_string(options['course_key'])
|
||||
except InvalidKeyError:
|
||||
raise CommandError("Invalid course key.")
|
||||
|
||||
if not modulestore().get_course(course_key):
|
||||
raise CommandError("Course not found.")
|
||||
|
||||
commit = False
|
||||
if len(args) == 2:
|
||||
commit = args[1] == 'commit'
|
||||
|
||||
# for now only support on split mongo
|
||||
owning_store = modulestore()._get_modulestore_for_courselike(course_key) # pylint: disable=protected-access
|
||||
if hasattr(owning_store, 'force_publish_course'):
|
||||
versions = get_course_versions(args[0])
|
||||
versions = get_course_versions(options['course_key'])
|
||||
print "Course versions : {0}".format(versions)
|
||||
|
||||
if commit:
|
||||
if options['commit']:
|
||||
if query_yes_no("Are you sure to publish the {0} course forcefully?".format(course_key), default="no"):
|
||||
# publish course forcefully
|
||||
updated_versions = owning_store.force_publish_course(
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, commit
|
||||
course_key, ModuleStoreEnum.UserID.mgmt_command, options['commit']
|
||||
)
|
||||
if updated_versions:
|
||||
# if publish and draft were different
|
||||
|
||||
@@ -7,8 +7,8 @@ import mock
|
||||
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from django.core.management import CommandError
|
||||
from contentstore.management.commands.delete_course import Command # pylint: disable=import-error
|
||||
from contentstore.tests.utils import CourseTestCase # pylint: disable=import-error
|
||||
from contentstore.management.commands.delete_course import Command
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestDeleteOrphan(TestOrphanBase):
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_delete_orphans_no_commit(self, default_store):
|
||||
"""
|
||||
Tests that running the command without a 'commit' argument
|
||||
Tests that running the command without a '--commit' argument
|
||||
results in no orphans being deleted
|
||||
"""
|
||||
course = self.create_course_with_orphans(default_store)
|
||||
@@ -37,12 +37,12 @@ class TestDeleteOrphan(TestOrphanBase):
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_delete_orphans_commit(self, default_store):
|
||||
"""
|
||||
Tests that running the command WITH the 'commit' argument
|
||||
Tests that running the command WITH the '--commit' argument
|
||||
results in the orphans being deleted
|
||||
"""
|
||||
course = self.create_course_with_orphans(default_store)
|
||||
|
||||
call_command('delete_orphans', unicode(course.id), commit='commit')
|
||||
call_command('delete_orphans', unicode(course.id), '--commit')
|
||||
|
||||
# make sure this module wasn't deleted
|
||||
self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html')))
|
||||
@@ -66,7 +66,7 @@ class TestDeleteOrphan(TestOrphanBase):
|
||||
|
||||
# call delete orphans, specifying the published branch
|
||||
# of the course
|
||||
call_command('delete_orphans', unicode(published_branch), commit='commit')
|
||||
call_command('delete_orphans', unicode(published_branch), '--commit')
|
||||
|
||||
# now all orphans should be deleted
|
||||
self.assertOrphanCount(course.id, 0)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Tests for the force_publish management command
|
||||
"""
|
||||
import mock
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.management import call_command, CommandError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -25,9 +25,9 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test 'force_publish' command with no arguments
|
||||
"""
|
||||
errstring = "force_publish requires 1 or more argument: <course_id> |commit"
|
||||
errstring = "Error: too few arguments"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle()
|
||||
call_command('force_publish')
|
||||
|
||||
def test_invalid_course_key(self):
|
||||
"""
|
||||
@@ -35,15 +35,15 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
"""
|
||||
errstring = "Invalid course key."
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle('TestX/TS01')
|
||||
call_command('force_publish', 'TestX/TS01')
|
||||
|
||||
def test_too_many_arguments(self):
|
||||
"""
|
||||
Test 'force_publish' command with more than 2 arguments
|
||||
"""
|
||||
errstring = "force_publish requires 1 or more argument: <course_id> |commit"
|
||||
errstring = "Error: unrecognized arguments: invalid-arg"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode(self.course.id), 'commit', 'invalid-arg')
|
||||
call_command('force_publish', unicode(self.course.id), '--commit', 'invalid-arg')
|
||||
|
||||
def test_course_key_not_found(self):
|
||||
"""
|
||||
@@ -51,7 +51,7 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
"""
|
||||
errstring = "Course not found."
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode('course-v1:org+course+run'))
|
||||
call_command('force_publish', unicode('course-v1:org+course+run'))
|
||||
|
||||
def test_force_publish_non_split(self):
|
||||
"""
|
||||
@@ -60,7 +60,7 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
errstring = 'The owning modulestore does not support this command.'
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
self.command.handle(unicode(course.id))
|
||||
call_command('force_publish', unicode(course.id))
|
||||
|
||||
@SharedModuleStoreTestCase.modifies_courseware
|
||||
def test_force_publish(self):
|
||||
@@ -91,7 +91,7 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
patched_yes_no.return_value = True
|
||||
|
||||
# force publish course
|
||||
self.command.handle(unicode(self.course.id), 'commit')
|
||||
call_command('force_publish', unicode(self.course.id), '--commit')
|
||||
|
||||
# verify that course has no changes
|
||||
self.assertFalse(self.store.has_changes(self.store.get_item(self.course.location)))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Models for contentstore
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
from django.db.models.fields import TextField
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""
|
||||
Test view handler for rerun (and eventually create)
|
||||
"""
|
||||
import ddt
|
||||
|
||||
from django.test.client import RequestFactory
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from student.tests.factories import UserFactory
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
|
||||
@@ -13,6 +17,7 @@ from datetime import datetime
|
||||
from xmodule.course_module import CourseFields
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseListing(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for getting the list of courses for a logged in user
|
||||
@@ -64,3 +69,21 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.assertEqual(dest_course_key.run, 'copy')
|
||||
dest_course = self.store.get_course(dest_course_key)
|
||||
self.assertEqual(dest_course.start, CourseFields.start.default)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_newly_created_course_has_web_certs_enabled(self, store):
|
||||
"""
|
||||
Tests newly created course has web certs enabled by default.
|
||||
"""
|
||||
with modulestore().default_store(store):
|
||||
response = self.client.ajax_post('/course/', {
|
||||
'org': 'orgX',
|
||||
'number': 'CS101',
|
||||
'display_name': 'Course with web certs enabled',
|
||||
'run': '2015_T2'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = parse_json(response)
|
||||
new_course_key = CourseKey.from_string(data['course_key'])
|
||||
course = self.store.get_course(new_course_key)
|
||||
self.assertTrue(course.cert_html_view_enabled)
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.conf import settings
|
||||
from course_modes.models import CourseMode
|
||||
from xmodule.library_tools import normalize_key_for_search
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
@@ -335,6 +335,25 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 5)
|
||||
|
||||
def _test_delete_course_from_search_index_after_course_deletion(self, store): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test that course will also be delete from search_index after course deletion.
|
||||
"""
|
||||
self.DOCUMENT_TYPE = 'course_info' # pylint: disable=invalid-name
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 0)
|
||||
|
||||
# index the course in search_index
|
||||
self.reindex_course(store)
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 1)
|
||||
|
||||
# delete the course and look course in search_index
|
||||
modulestore().delete_course(self.course.id, self.user_id)
|
||||
self.assertIsNone(modulestore().get_course(self.course.id))
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 0)
|
||||
|
||||
def _test_deleting_item(self, store):
|
||||
""" test deleting an item """
|
||||
# Publish the vertical to start with
|
||||
@@ -604,6 +623,11 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
def test_course_location_null(self, store_type):
|
||||
self._perform_test_using_store(store_type, self._test_course_location_null)
|
||||
|
||||
@ddt.data(*WORKS_WITH_STORES)
|
||||
def test_delete_course_from_search_index_after_course_deletion(self, store_type):
|
||||
""" Test for removing course from CourseAboutSearchIndexer """
|
||||
self._perform_test_using_store(store_type, self._test_delete_course_from_search_index_after_course_deletion)
|
||||
|
||||
|
||||
@patch('django.conf.settings.SEARCH_ENGINE', 'search.tests.utils.ForceRefreshElasticSearchEngine')
|
||||
@ddt.ddt
|
||||
|
||||
@@ -105,7 +105,7 @@ class LibraryTestCase(ModuleStoreTestCase):
|
||||
if user not in self.session_data:
|
||||
self.session_data[user] = {}
|
||||
request = Mock(user=user, session=self.session_data[user])
|
||||
_load_preview_module(request, descriptor) # pylint: disable=protected-access
|
||||
_load_preview_module(request, descriptor)
|
||||
|
||||
def _update_item(self, usage_key, metadata):
|
||||
"""
|
||||
|
||||
@@ -10,8 +10,8 @@ from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
|
||||
|
||||
from contentstore.utils import reverse_url # pylint: disable=import-error
|
||||
from student.models import Registration # pylint: disable=import-error
|
||||
from contentstore.utils import reverse_url
|
||||
from student.models import Registration
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, fixme
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
"All view functions for contentstore, broken out into submodules"
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class CertificateManager(object):
|
||||
"""
|
||||
for cert_index, cert in enumerate(course.certificates['certificates']): # pylint: disable=unused-variable
|
||||
if int(cert['id']) == int(certificate_id):
|
||||
for sig_index, signatory in enumerate(cert.get('signatories')): # pylint: disable=unused-variable
|
||||
for sig_index, signatory in enumerate(cert.get('signatories')):
|
||||
if int(signatory_id) == int(signatory['id']):
|
||||
_delete_asset(course.id, signatory['signature_image_path'])
|
||||
del cert['signatories'][sig_index]
|
||||
|
||||
@@ -21,7 +21,6 @@ from django.utils.translation import ugettext
|
||||
__all__ = ['checklists_handler']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -82,7 +82,6 @@ def _load_mixed_class(category):
|
||||
return mixologist.mix(component_class)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_GET
|
||||
@login_required
|
||||
def container_handler(request, usage_key_string):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.shortcuts import redirect
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import string # pylint: disable=deprecated-module
|
||||
import string
|
||||
from django.utils.translation import ugettext as _
|
||||
import django.utils
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -219,7 +219,6 @@ def _dismiss_notification(request, course_action_state_id): # pylint: disable=u
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
def course_handler(request, course_key_string=None):
|
||||
"""
|
||||
@@ -741,8 +740,11 @@ def create_new_course_in_store(store, user, org, number, run, fields):
|
||||
Separated out b/c command line course creation uses this as well as the web interface.
|
||||
"""
|
||||
|
||||
# Set default language from settings
|
||||
fields.update({'language': getattr(settings, 'DEFAULT_COURSE_LANGUAGE', 'en')})
|
||||
# Set default language from settings and enable web certs
|
||||
fields.update({
|
||||
'language': getattr(settings, 'DEFAULT_COURSE_LANGUAGE', 'en'),
|
||||
'cert_html_view_enabled': True,
|
||||
})
|
||||
|
||||
with modulestore().default_store(store):
|
||||
# Creating the course raises DuplicateCourseError if an existing course with this org/name is found
|
||||
@@ -803,7 +805,6 @@ def _rerun_course(request, org, number, run, fields):
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
@@ -836,7 +837,6 @@ def course_info_handler(request, course_key_string):
|
||||
return HttpResponseBadRequest("Only supports html requests")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
|
||||
@@ -177,7 +177,7 @@ def _get_entrance_exam(request, course_key): # pylint: disable=W0613
|
||||
course = modulestore().get_course(course_key)
|
||||
if course is None:
|
||||
return HttpResponse(status=400)
|
||||
if not getattr(course, 'entrance_exam_id'):
|
||||
if not course.entrance_exam_id:
|
||||
return HttpResponse(status=404)
|
||||
try:
|
||||
exam_key = UsageKey.from_string(course.entrance_exam_id)
|
||||
@@ -228,7 +228,7 @@ def _delete_entrance_exam(request, course_key):
|
||||
# Reset the entrance exam flags on the course
|
||||
# Reload the course so we have the latest state
|
||||
course = store.get_course(course_key)
|
||||
if getattr(course, 'entrance_exam_id'):
|
||||
if course.entrance_exam_id:
|
||||
metadata = {
|
||||
'entrance_exam_enabled': False,
|
||||
'entrance_exam_minimum_score_pct': None,
|
||||
|
||||
@@ -59,7 +59,6 @@ log = logging.getLogger(__name__)
|
||||
CONTENT_RE = re.compile(r"(?P<start>\d{1,11})-(?P<stop>\d{1,11})/(?P<end>\d{1,11})")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@@ -359,7 +358,6 @@ def _save_request_status(request, key, status):
|
||||
request.session.save()
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_GET
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@@ -458,7 +456,6 @@ def send_tarball(tarball):
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
@require_http_methods(("GET",))
|
||||
|
||||
@@ -88,7 +88,6 @@ def _filter_entrance_exam_grader(graders):
|
||||
return graders
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_http_methods(("DELETE", "GET", "PUT", "POST", "PATCH"))
|
||||
@login_required
|
||||
@expect_json
|
||||
@@ -196,7 +195,6 @@ def xblock_handler(request, usage_key_string):
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_http_methods(("GET"))
|
||||
@login_required
|
||||
@expect_json
|
||||
@@ -259,7 +257,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
'page_size': int(request.REQUEST.get('page_size', 0)),
|
||||
}
|
||||
except ValueError:
|
||||
# pylint: disable=too-many-format-args
|
||||
return HttpResponse(
|
||||
content="Couldn't parse paging parameters: enable_paging: "
|
||||
"{0}, page_number: {1}, page_size: {2}".format(
|
||||
@@ -312,7 +309,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
return HttpResponse(status=406)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_http_methods(("GET"))
|
||||
@login_required
|
||||
@expect_json
|
||||
@@ -663,7 +659,6 @@ def _delete_item(usage_key, user):
|
||||
store.delete_item(usage_key, user.id)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@require_http_methods(("GET", "DELETE"))
|
||||
def orphan_handler(request, course_key_string):
|
||||
|
||||
@@ -230,7 +230,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertIn("Location", response)
|
||||
content = json.loads(response.content)
|
||||
certificate_id = self._remove_ids(content) # pylint: disable=unused-variable
|
||||
certificate_id = self._remove_ids(content)
|
||||
self.assertEqual(content, expected)
|
||||
self.assert_event_emitted(
|
||||
'edx.certificate.configuration.created',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""
|
||||
Unit tests for video-related REST APIs.
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
import csv
|
||||
import json
|
||||
import dateutil.parser
|
||||
|
||||
@@ -34,7 +34,6 @@ def request_course_creator(request):
|
||||
return JsonResponse({"Status": "OK"})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
|
||||
|
||||
@@ -269,6 +269,8 @@ else:
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
|
||||
# Enable automatic transaction management on all databases
|
||||
# https://docs.djangoproject.com/en/1.8/topics/db/transactions/#tying-transactions-to-http-requests
|
||||
# This needs to be true for all databases
|
||||
for database_name in DATABASES:
|
||||
DATABASES[database_name]['ATOMIC_REQUESTS'] = True
|
||||
|
||||
|
||||
@@ -13,11 +13,6 @@ from the same directory.
|
||||
import os
|
||||
from path import Path as path
|
||||
|
||||
# Pylint gets confused by path.py instances, which report themselves as class
|
||||
# objects. As a result, pylint applies the wrong regex in validating names,
|
||||
# and throws spurious errors. Therefore, we disable invalid-name checking.
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
########################## Prod-like settings ###################################
|
||||
# These should be as close as possible to the settings we use in production.
|
||||
|
||||
@@ -22,12 +22,7 @@ Longer TODO:
|
||||
|
||||
# We intentionally define lots of variables that aren't used, and
|
||||
# want to import all variables from base settings files
|
||||
# pylint: disable=wildcard-import, unused-import, unused-wildcard-import
|
||||
|
||||
# Pylint gets confused by path.py instances, which report themselves as class
|
||||
# objects. As a result, pylint applies the wrong regex in validating names,
|
||||
# and throws spurious errors. Therefore, we disable invalid-name checking.
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=unused-import
|
||||
|
||||
import imp
|
||||
import os
|
||||
@@ -62,7 +57,7 @@ from xmodule.mixin import LicenseMixin
|
||||
|
||||
|
||||
# Dummy secret key for dev/test
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
SECRET_KEY = 'dev key'
|
||||
|
||||
STUDIO_NAME = "Studio"
|
||||
STUDIO_SHORT_NAME = "Studio"
|
||||
@@ -1014,6 +1009,8 @@ ADVANCED_COMPONENT_TYPES = [
|
||||
'notes',
|
||||
'schoolyourself_review',
|
||||
'schoolyourself_lesson',
|
||||
# Office Mix
|
||||
'officemix',
|
||||
|
||||
# Google Drive embedded components. These XBlocks allow one to
|
||||
# embed public google drive documents and calendars within edX units
|
||||
|
||||
@@ -35,9 +35,6 @@ var CourseDetails = Backbone.Model.extend({
|
||||
if (newattrs.start_date === null) {
|
||||
errors.start_date = gettext("The course must have an assigned start date.");
|
||||
}
|
||||
if (this.hasChanged("start_date") && this.get("has_cert_config") === false){
|
||||
errors.start_date = gettext("The course must have at least one active certificate configuration before it can be started.");
|
||||
}
|
||||
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = gettext("The course end date must be later than the course start date.");
|
||||
}
|
||||
|
||||
@@ -72,13 +72,6 @@ define([
|
||||
);
|
||||
});
|
||||
|
||||
it('Changing course start date without active certificate configuration should result in error', function () {
|
||||
this.view.$el.find('#course-start-date')
|
||||
.val('10/06/2014')
|
||||
.trigger('change');
|
||||
expect(this.view.$el.find('span.message-error').text()).toContain("course must have at least one active certificate configuration");
|
||||
});
|
||||
|
||||
it('Selecting a course in pre-requisite drop down should save it as part of course details', function () {
|
||||
var pre_requisite_courses = ['test/CSS101/2012_T1'];
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
|
||||
@@ -288,7 +288,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
var template = _.template(
|
||||
'<li class="list-settings-item">' +
|
||||
'<input type="text" class="input" value="<%= ele %>">' +
|
||||
'<a href="#" class="remove-action remove-setting" data-index="<%= index %>"><i class="icon fa fa-remove-sign"></i><span class="sr">Remove</span></a>' +
|
||||
'<a href="#" class="remove-action remove-setting" data-index="<%= index %>"><i class="icon fa fa-times-circle" aria-hidden="true"></i><span class="sr">Remove</span></a>' +
|
||||
'</li>'
|
||||
);
|
||||
list.append($(template({'ele': ele, 'index': index})));
|
||||
@@ -455,7 +455,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog,
|
||||
'<li class="list-settings-item">' +
|
||||
'<input type="text" class="input input-key" value="<%= key %>">' +
|
||||
'<input type="text" class="input input-value" value="<%= value %>">' +
|
||||
'<a href="#" class="remove-action remove-setting" data-value="<%= value %>"><i class="icon fa fa-remove-sign"></i><span class="sr">Remove</span></a>' +
|
||||
'<a href="#" class="remove-action remove-setting" data-value="<%= value %>"><i class="icon fa fa-times-circle" aria-hidden="true"></i><span class="sr">Remove</span></a>' +
|
||||
'</li>'
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-<%= name %>"
|
||||
aria-describedby="modal-window-description"
|
||||
aria-labelledby="modal-window-title"
|
||||
aria-hidden=""
|
||||
role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div class="modal-window <%= viewSpecificClasses %> modal-<%= size %> modal-type-<%= type %>" tabindex="-1" aria-labelledby="modal-window-title">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-edit-xblock" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-edit-xblock" aria-labelledby="modal-window-title" role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div class="modal-window confirm modal-med modal-type-html modal-editor" style="top: 50px; left: 400px;" tabindex="-1" aria-labelledby="modal-window-title">
|
||||
<div class="edit-xblock-modal">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-section" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-section" aria-labelledby="modal-window-title" role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
|
||||
<div class="bulkpublish-section-modal">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-subsection" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-subsection" aria-labelledby="modal-window-title" role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
|
||||
<div class="bulkpublish-subsection-modal">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-unit" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
|
||||
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-unit" aria-labelledby="modal-window-title" role="dialog">
|
||||
<div class="modal-window-overlay"></div>
|
||||
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
|
||||
<div class="bulkpublish-unit-modal">
|
||||
|
||||
@@ -5,8 +5,6 @@ from django.conf.urls import patterns, include, url
|
||||
from ratelimitbackend import admin
|
||||
admin.autodiscover()
|
||||
|
||||
# pylint: disable=bad-continuation
|
||||
|
||||
# Pattern to match a course key or a library key
|
||||
COURSELIKE_KEY_PATTERN = r'(?P<course_key_string>({}|{}))'.format(
|
||||
r'[^/]+/[^/]+/[^/]+', r'[^/:]+:[^/+]+\+[^/+]+(\+[^/]+)?'
|
||||
|
||||
@@ -344,7 +344,6 @@ class ConfigurationModelAPITests(TestCase):
|
||||
request = self.factory.get('/config/ExampleConfig')
|
||||
request.user = self.user
|
||||
response = self.current_view(request)
|
||||
# pylint: disable=no-member
|
||||
self.assertEquals('', response.data['string_field'])
|
||||
self.assertEquals(10, response.data['int_field'])
|
||||
self.assertEquals(None, response.data['changed_by'])
|
||||
|
||||
@@ -51,7 +51,7 @@ def xdomain_proxy(request): # pylint: disable=unused-argument
|
||||
return HttpResponseNotFound()
|
||||
|
||||
allowed_domains = []
|
||||
for domain in config.whitelist.split("\n"): # pylint: disable=no-member
|
||||
for domain in config.whitelist.split("\n"):
|
||||
if domain.strip():
|
||||
allowed_domains.append(domain.strip())
|
||||
|
||||
|
||||
@@ -538,7 +538,7 @@ class CourseMode(models.Model):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def min_course_price_for_currency(cls, course_id, currency): # pylint: disable=invalid-name
|
||||
def min_course_price_for_currency(cls, course_id, currency):
|
||||
"""
|
||||
Returns the minimum price of the course in the appropriate currency over all the course's
|
||||
non-expired modes.
|
||||
|
||||
@@ -22,10 +22,10 @@ class DarkLangConfig(ConfigurationModel):
|
||||
|
||||
Example: ['it', 'de-at', 'es', 'pt-br']
|
||||
"""
|
||||
if not self.released_languages.strip(): # pylint: disable=no-member
|
||||
if not self.released_languages.strip():
|
||||
return []
|
||||
|
||||
languages = [lang.lower().strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member
|
||||
languages = [lang.lower().strip() for lang in self.released_languages.split(',')]
|
||||
# Put in alphabetical order
|
||||
languages.sort()
|
||||
return languages
|
||||
|
||||
@@ -92,7 +92,7 @@ class Role(models.Model):
|
||||
if permission_blacked_out(course, {self.name}, permission):
|
||||
return False
|
||||
|
||||
return self.permissions.filter(name=permission).exists() # pylint: disable=no-member
|
||||
return self.permissions.filter(name=permission).exists()
|
||||
|
||||
|
||||
class Permission(models.Model):
|
||||
|
||||
@@ -88,7 +88,7 @@ class EmbargoedState(ConfigurationModel):
|
||||
"""
|
||||
if self.embargoed_countries == '':
|
||||
return []
|
||||
return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member
|
||||
return [country.strip().upper() for country in self.embargoed_countries.split(',')]
|
||||
|
||||
|
||||
class RestrictedCourse(models.Model):
|
||||
@@ -703,7 +703,7 @@ class IPFilter(ConfigurationModel):
|
||||
"""
|
||||
if self.whitelist == '':
|
||||
return []
|
||||
return self.IPFilterList([addr.strip() for addr in self.whitelist.split(',')]) # pylint: disable=no-member
|
||||
return self.IPFilterList([addr.strip() for addr in self.whitelist.split(',')])
|
||||
|
||||
@property
|
||||
def blacklist_ips(self):
|
||||
@@ -712,4 +712,4 @@ class IPFilter(ConfigurationModel):
|
||||
"""
|
||||
if self.blacklist == '':
|
||||
return []
|
||||
return self.IPFilterList([addr.strip() for addr in self.blacklist.split(',')]) # pylint: disable=no-member
|
||||
return self.IPFilterList([addr.strip() for addr in self.blacklist.split(',')])
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import string # pylint: disable=deprecated-module
|
||||
import string
|
||||
import fnmatch
|
||||
import unicodedata
|
||||
import urllib
|
||||
|
||||
@@ -36,10 +36,9 @@ class MicroSiteSessionCookieTests(TestCase):
|
||||
"""
|
||||
Tests that non-microsite behaves according to default behavior
|
||||
"""
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertNotIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid']))
|
||||
self.assertNotIn('Domain', str(getattr(response, 'cookies')['sessionid']))
|
||||
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
|
||||
self.assertNotIn('Domain', str(response.cookies['sessionid'])) # pylint: disable=no-member
|
||||
|
||||
def test_session_cookie_domain(self):
|
||||
"""
|
||||
@@ -47,9 +46,8 @@ class MicroSiteSessionCookieTests(TestCase):
|
||||
is the one specially overridden in configuration,
|
||||
in this case in test.py
|
||||
"""
|
||||
|
||||
response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
self.assertIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid']))
|
||||
self.assertIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
|
||||
|
||||
@patch.dict("django.conf.settings.MICROSITE_CONFIGURATION", {'test_microsite': {'SESSION_COOKIE_DOMAIN': None}})
|
||||
def test_microsite_none_cookie_domain(self):
|
||||
@@ -57,7 +55,6 @@ class MicroSiteSessionCookieTests(TestCase):
|
||||
Tests to make sure that a Microsite that specifies None for 'SESSION_COOKIE_DOMAIN' does not
|
||||
set a domain on the session cookie
|
||||
"""
|
||||
|
||||
response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
self.assertNotIn('test_microsite.localhost', str(getattr(response, 'cookies')['sessionid']))
|
||||
self.assertNotIn('Domain', str(getattr(response, 'cookies')['sessionid']))
|
||||
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
|
||||
self.assertNotIn('Domain', str(response.cookies['sessionid'])) # pylint: disable=no-member
|
||||
|
||||
@@ -53,7 +53,7 @@ class Command(TrackedCommand):
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options): # pylint: disable=unused-argument
|
||||
def handle(self, *args, **options):
|
||||
source_key = CourseKey.from_string(options.get('source_course', ''))
|
||||
dest_keys = []
|
||||
for course_key in options.get('dest_course_list', '').split(','):
|
||||
|
||||
@@ -1085,7 +1085,7 @@ class CourseEnrollment(models.Model):
|
||||
log.exception(
|
||||
u'Unable to emit event %s for user %s and course %s',
|
||||
event_name,
|
||||
self.user.username, # pylint: disable=no-member
|
||||
self.user.username,
|
||||
self.course_id,
|
||||
)
|
||||
|
||||
@@ -1373,7 +1373,7 @@ class CourseEnrollment(models.Model):
|
||||
def refund_cutoff_date(self):
|
||||
""" Calculate and return the refund window end date. """
|
||||
try:
|
||||
attribute = self.attributes.get(namespace='order', name='order_number') # pylint: disable=no-member
|
||||
attribute = self.attributes.get(namespace='order', name='order_number')
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def register_access_role(cls):
|
||||
|
||||
"""
|
||||
try:
|
||||
role_name = getattr(cls, 'ROLE')
|
||||
role_name = cls.ROLE
|
||||
REGISTERED_ACCESS_ROLES[role_name] = cls
|
||||
except AttributeError:
|
||||
log.exception(u"Unable to register Access Role with attribute 'ROLE'.")
|
||||
@@ -62,7 +62,7 @@ class AccessRole(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def has_user(self, user): # pylint: disable=unused-argument
|
||||
def has_user(self, user):
|
||||
"""
|
||||
Return whether the supplied django user has access to this role.
|
||||
"""
|
||||
|
||||
@@ -285,7 +285,7 @@ class LoginTest(TestCase):
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
# Reload the user from the database
|
||||
self.user = User.objects.get(pk=self.user.pk) # pylint: disable=no-member
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
|
||||
self.assertEqual(self.user.profile.get_meta()['session_id'], client1.session.session_key)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from student.models import CourseEnrollment, DashboardConfiguration
|
||||
from student.views import get_course_enrollments, _get_recently_enrolled_courses # pylint: disable=protected-access
|
||||
from student.views import get_course_enrollments, _get_recently_enrolled_courses
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
|
||||
@@ -148,7 +148,7 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
self.enrollment.course_overview.start = course_start
|
||||
self.enrollment.attributes.add(CourseEnrollmentAttribute( # pylint: disable=no-member
|
||||
self.enrollment.attributes.add(CourseEnrollmentAttribute(
|
||||
enrollment=self.enrollment,
|
||||
namespace='order',
|
||||
name='order_number',
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestStudentDashboardUnenrollments(ModuleStoreTestCase):
|
||||
self.cert_status = None
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
|
||||
def mock_cert(self, _user, _course_overview, _course_mode): # pylint: disable=unused-argument
|
||||
def mock_cert(self, _user, _course_overview, _course_mode):
|
||||
""" Return a preset certificate status. """
|
||||
if self.cert_status is not None:
|
||||
return {
|
||||
|
||||
@@ -131,7 +131,7 @@ from openedx.core.djangoapps.programs.utils import is_student_dashboard_programs
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=invalid-name
|
||||
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display')
|
||||
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
|
||||
# we have to check only for the invoice generated registration codes
|
||||
# that their invoice is valid or not
|
||||
if redeemed_registration.invoice_item:
|
||||
if not getattr(redeemed_registration.invoice_item.invoice, 'is_valid'):
|
||||
if not redeemed_registration.invoice_item.invoice.is_valid:
|
||||
blocked = True
|
||||
# disabling email notifications for unpaid registration courses
|
||||
Optout.objects.get_or_create(user=request.user, course_id=course_key)
|
||||
@@ -1632,7 +1632,7 @@ def create_account_with_params(request, params):
|
||||
if hasattr(settings, 'LMS_SEGMENT_KEY') and settings.LMS_SEGMENT_KEY:
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
identity_args = [
|
||||
user.id, # pylint: disable=no-member
|
||||
user.id,
|
||||
{
|
||||
'email': user.email,
|
||||
'username': user.username,
|
||||
@@ -1895,13 +1895,13 @@ def auto_auth(request):
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'user_id': user.id, # pylint: disable=no-member
|
||||
'user_id': user.id,
|
||||
'anonymous_id': anonymous_id_for_user(user, None),
|
||||
})
|
||||
else:
|
||||
success_msg = u"{} user {} ({}) with password {} and user_id {}".format(
|
||||
u"Logged in" if login_when_done else "Created",
|
||||
username, email, password, user.id # pylint: disable=no-member
|
||||
username, email, password, user.id
|
||||
)
|
||||
response = HttpResponse(success_msg)
|
||||
response.set_cookie('csrftoken', csrf(request)['csrf_token'])
|
||||
@@ -2285,7 +2285,7 @@ def change_email_settings(request):
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
|
||||
def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invalid-name
|
||||
def _get_course_programs(user, user_enrolled_courses):
|
||||
""" Returns a dictionary of programs courses data require for the student
|
||||
dashboard.
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
import urllib
|
||||
from lettuce import world
|
||||
|
||||
@@ -43,7 +43,7 @@ def start_video_server():
|
||||
video_source_dir = '{}/data/video'.format(settings.TEST_ROOT)
|
||||
video_server = VideoSourceHttpService(port_num=settings.VIDEO_SOURCE_PORT)
|
||||
video_server.config['root_dir'] = video_source_dir
|
||||
setattr(world, 'video_source', video_server)
|
||||
world.video_source = video_server
|
||||
|
||||
|
||||
@after.all # pylint: disable=no-member
|
||||
|
||||
@@ -255,7 +255,7 @@ class StubLtiHandler(StubHttpRequestHandler):
|
||||
# Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
|
||||
sha1 = hashlib.sha1()
|
||||
sha1.update(body)
|
||||
oauth_body_hash = unicode(base64.b64encode(sha1.digest())) # pylint: disable=too-many-function-args
|
||||
oauth_body_hash = unicode(base64.b64encode(sha1.digest()))
|
||||
params = client.get_oauth_params(None)
|
||||
params.append((u'oauth_body_hash', oauth_body_hash))
|
||||
mock_request = mock.Mock(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
from lettuce import world
|
||||
|
||||
@@ -22,7 +21,7 @@ from selenium.common.exceptions import (
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from nose.tools import assert_true # pylint: disable=no-name-in-module
|
||||
from nose.tools import assert_true
|
||||
|
||||
GLOBAL_WAIT_FOR_TIMEOUT = 60
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ admin.site.register(LTIProviderConfig, LTIProviderConfigAdmin)
|
||||
|
||||
class ApiPermissionsAdminForm(forms.ModelForm):
|
||||
""" Django admin form for ApiPermissions model """
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
class Meta(object):
|
||||
model = ProviderApiPermissions
|
||||
fields = ['client', 'provider_id']
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class AuthNotConfigured(SocialAuthBaseException):
|
||||
self.provider_name = provider_name
|
||||
|
||||
def __str__(self):
|
||||
return _('Authentication with {} is currently unavailable.').format( # pylint: disable=no-member
|
||||
return _('Authentication with {} is currently unavailable.').format(
|
||||
self.provider_name
|
||||
)
|
||||
|
||||
@@ -590,7 +590,7 @@ class ProviderApiPermissions(models.Model):
|
||||
)
|
||||
)
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
class Meta(object):
|
||||
app_label = "third_party_auth"
|
||||
verbose_name = "Provider API Permission"
|
||||
verbose_name_plural = verbose_name + 's'
|
||||
|
||||
@@ -58,7 +58,7 @@ See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
|
||||
"""
|
||||
|
||||
import random
|
||||
import string # pylint: disable=deprecated-module
|
||||
import string
|
||||
from collections import OrderedDict
|
||||
import urllib
|
||||
import analytics
|
||||
@@ -151,19 +151,6 @@ class AuthEntryError(AuthException):
|
||||
"""
|
||||
|
||||
|
||||
class NotActivatedException(AuthException):
|
||||
""" Raised when a user tries to login to an unverified account """
|
||||
def __init__(self, backend, email):
|
||||
self.email = email
|
||||
super(NotActivatedException, self).__init__(backend, email)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
_('This account has not yet been activated. An activation email has been re-sent to {email_address}.')
|
||||
.format(email_address=self.email)
|
||||
)
|
||||
|
||||
|
||||
class ProviderUserState(object):
|
||||
"""Object representing the provider state (attached or not) for a user.
|
||||
|
||||
@@ -514,26 +501,27 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
|
||||
# This parameter is used by the auth_exchange app, which always allows users to
|
||||
# login, whether or not their account is validated.
|
||||
pass
|
||||
# IF the user has just registered a new account as part of this pipeline, that is fine
|
||||
# and we allow the login to continue this once, because if we pause again to force the
|
||||
# user to activate their account via email, the pipeline may get lost (e.g. email takes
|
||||
# too long to arrive, user opens the activation email on a different device, etc.).
|
||||
# This is consistent with first party auth and ensures that the pipeline completes
|
||||
# fully, which is critical.
|
||||
# But if this is an existing account, we refuse to allow them to login again until they
|
||||
# check their email and activate the account.
|
||||
elif social is not None:
|
||||
# This third party account is already linked to a user account. That means that the
|
||||
# user's account existed before this pipeline originally began (since the creation
|
||||
# of the 'social' link entry occurs in one of the following pipeline steps).
|
||||
# Reject this login attempt and tell the user to validate their account first.
|
||||
|
||||
# Send them another activation email:
|
||||
student.views.reactivation_email_for_user(user)
|
||||
|
||||
raise NotActivatedException(backend, user.email)
|
||||
# else: The user must have just successfully registered their account, so we proceed.
|
||||
# We know they did not just login, because the login process rejects unverified users.
|
||||
elif social is None:
|
||||
# The user has just registered a new account as part of this pipeline. Their account
|
||||
# is inactive but we allow the login to continue, because if we pause again to force
|
||||
# the user to activate their account via email, the pipeline may get lost (e.g.
|
||||
# email takes too long to arrive, user opens the activation email on a different
|
||||
# device, etc.). This is consistent with first party auth and ensures that the
|
||||
# pipeline completes fully, which is critical.
|
||||
pass
|
||||
else:
|
||||
# This is an existing account, linked to a third party provider but not activated.
|
||||
# Double-check these criteria:
|
||||
assert user is not None
|
||||
assert social is not None
|
||||
# We now also allow them to login again, because if they had entered their email
|
||||
# incorrectly then there would be no way for them to recover the account, nor
|
||||
# register anew via SSO. See SOL-1324 in JIRA.
|
||||
# However, we will log a warning for this case:
|
||||
logger.warning(
|
||||
'User "%s" is using third_party_auth to login but has not yet activated their account. ',
|
||||
user.username
|
||||
)
|
||||
|
||||
|
||||
@partial.partial
|
||||
@@ -601,7 +589,7 @@ def login_analytics(strategy, auth_entry, *args, **kwargs):
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': None,
|
||||
'provider': getattr(kwargs['backend'], 'name')
|
||||
'provider': kwargs['backend'].name
|
||||
},
|
||||
context={
|
||||
'ip': tracking_context.get('ip'),
|
||||
|
||||
@@ -67,11 +67,16 @@ def apply_settings(django_settings):
|
||||
django_settings.SOCIAL_AUTH_RAISE_EXCEPTIONS = False
|
||||
|
||||
# Allow users to login using social auth even if their account is not verified yet
|
||||
# The 'ensure_user_information' step controls this and only allows brand new users
|
||||
# to login without verification. Repeat logins are not permitted until the account
|
||||
# gets verified.
|
||||
django_settings.INACTIVE_USER_LOGIN = True
|
||||
django_settings.INACTIVE_USER_URL = '/auth/inactive'
|
||||
# This is required since we [ab]use django's 'is_active' flag to indicate verified
|
||||
# accounts; without this set to True, python-social-auth won't allow us to link the
|
||||
# user's account to the third party account during registration (since the user is
|
||||
# not verified at that point).
|
||||
# We also generally allow unverified third party auth users to login (see the logic
|
||||
# in ensure_user_information in pipeline.py) because otherwise users who use social
|
||||
# auth to register with an invalid email address can become "stuck".
|
||||
# TODO: Remove the following if/when email validation is separated from the is_active flag.
|
||||
django_settings.SOCIAL_AUTH_INACTIVE_USER_LOGIN = True
|
||||
django_settings.SOCIAL_AUTH_INACTIVE_USER_URL = '/auth/inactive'
|
||||
|
||||
# Context processors required under Django.
|
||||
django_settings.SOCIAL_AUTH_UUID_LENGTH = 4
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"""
|
||||
Code to manage fetching and storing the metadata of IdPs.
|
||||
"""
|
||||
#pylint: disable=no-member
|
||||
from celery.task import task # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
from celery.task import task
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import logging
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.contrib import auth
|
||||
from django.contrib.auth import models as auth_models
|
||||
from django.contrib.messages.storage import fallback
|
||||
from django.contrib.sessions.backends import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import utils as django_utils
|
||||
from django.conf import settings as django_settings
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
@@ -18,6 +19,7 @@ from social.apps.django_app import utils as social_utils
|
||||
from social.apps.django_app import views as social_views
|
||||
from student import models as student_models
|
||||
from student import views as student_views
|
||||
from student.tests.factories import UserFactory
|
||||
from student_account.views import account_settings_context
|
||||
|
||||
from third_party_auth import middleware, pipeline
|
||||
@@ -25,6 +27,176 @@ from third_party_auth import settings as auth_settings
|
||||
from third_party_auth.tests import testutil
|
||||
|
||||
|
||||
class IntegrationTestMixin(object):
|
||||
"""
|
||||
Mixin base class for third_party_auth integration tests.
|
||||
This class is newer and simpler than the 'IntegrationTest' alternative below, but it is
|
||||
currently less comprehensive. Some providers are tested with this, others with
|
||||
IntegrationTest.
|
||||
"""
|
||||
# Provider information:
|
||||
PROVIDER_NAME = "override"
|
||||
PROVIDER_BACKEND = "override"
|
||||
PROVIDER_ID = "override"
|
||||
# Information about the user expected from the provider:
|
||||
USER_EMAIL = "override"
|
||||
USER_NAME = "override"
|
||||
USER_USERNAME = "override"
|
||||
|
||||
def setUp(self):
|
||||
super(IntegrationTestMixin, self).setUp()
|
||||
self.login_page_url = reverse('signin_user')
|
||||
self.register_page_url = reverse('register_user')
|
||||
patcher = testutil.patch_mako_templates()
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
# Override this method in a subclass and enable at least one provider.
|
||||
|
||||
def test_register(self):
|
||||
# The user goes to the register page, and sees a button to register with the provider:
|
||||
provider_register_url = self._check_register_page()
|
||||
# The user clicks on the Dummy button:
|
||||
try_login_response = self.client.get(provider_register_url)
|
||||
# The user should be redirected to the provider's login page:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
provider_response = self.do_provider_login(try_login_response['Location'])
|
||||
# We should be redirected to the register screen since this account is not linked to an edX account:
|
||||
self.assertEqual(provider_response.status_code, 302)
|
||||
self.assertEqual(provider_response['Location'], self.url_prefix + self.register_page_url)
|
||||
register_response = self.client.get(self.register_page_url)
|
||||
tpa_context = register_response.context["data"]["third_party_auth"]
|
||||
self.assertEqual(tpa_context["errorMessage"], None)
|
||||
# Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown.
|
||||
self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME)
|
||||
# Check that the data (e.g. email) from the provider is displayed in the form:
|
||||
form_data = register_response.context['data']['registration_form_desc']
|
||||
form_fields = {field['name']: field for field in form_data['fields']}
|
||||
self.assertEqual(form_fields['email']['defaultValue'], self.USER_EMAIL)
|
||||
self.assertEqual(form_fields['name']['defaultValue'], self.USER_NAME)
|
||||
self.assertEqual(form_fields['username']['defaultValue'], self.USER_USERNAME)
|
||||
# Now complete the form:
|
||||
ajax_register_response = self.client.post(
|
||||
reverse('user_api_registration'),
|
||||
{
|
||||
'email': 'email-edited@tpa-test.none',
|
||||
'name': 'My Customized Name',
|
||||
'username': 'new_username',
|
||||
'honor_code': True,
|
||||
}
|
||||
)
|
||||
self.assertEqual(ajax_register_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(tpa_context["finishAuthUrl"])
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
|
||||
# Now check that we can login again, whether or not we have yet verified the account:
|
||||
self.client.logout()
|
||||
self._test_return_login(user_is_activated=False)
|
||||
|
||||
self.client.logout()
|
||||
self.verify_user_email('email-edited@tpa-test.none')
|
||||
self._test_return_login(user_is_activated=True)
|
||||
|
||||
def test_login(self):
|
||||
user = UserFactory.create()
|
||||
# The user goes to the login page, and sees a button to login with this provider:
|
||||
provider_login_url = self._check_login_page()
|
||||
# The user clicks on the provider's button:
|
||||
try_login_response = self.client.get(provider_login_url)
|
||||
# The user should be redirected to the provider's login page:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
complete_response = self.do_provider_login(try_login_response['Location'])
|
||||
# We should be redirected to the login screen since this account is not linked to an edX account:
|
||||
self.assertEqual(complete_response.status_code, 302)
|
||||
self.assertEqual(complete_response['Location'], self.url_prefix + self.login_page_url)
|
||||
login_response = self.client.get(self.login_page_url)
|
||||
tpa_context = login_response.context["data"]["third_party_auth"]
|
||||
self.assertEqual(tpa_context["errorMessage"], None)
|
||||
# Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown.
|
||||
self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME)
|
||||
# Now the user enters their username and password.
|
||||
# The AJAX on the page will log them in:
|
||||
ajax_login_response = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
{'email': user.email, 'password': 'test'}
|
||||
)
|
||||
self.assertEqual(ajax_login_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(tpa_context["finishAuthUrl"])
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self._test_return_login()
|
||||
|
||||
def do_provider_login(self, provider_redirect_url):
|
||||
"""
|
||||
mock logging in to the provider
|
||||
Should end with loading self.complete_url, which should be returned
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _test_return_login(self, user_is_activated=True):
|
||||
""" Test logging in to an account that is already linked. """
|
||||
# Make sure we're not logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 302)
|
||||
# The user goes to the login page, and sees a button to login with this provider:
|
||||
provider_login_url = self._check_login_page()
|
||||
# The user clicks on the provider's login button:
|
||||
try_login_response = self.client.get(provider_login_url)
|
||||
# The user should be redirected to the provider:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
login_response = self.do_provider_login(try_login_response['Location'])
|
||||
# There will be one weird redirect required to set the login cookie:
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + self.complete_url)
|
||||
# And then we should be redirected to the dashboard:
|
||||
login_response = self.client.get(login_response['Location'])
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
if user_is_activated:
|
||||
url_expected = reverse('dashboard')
|
||||
else:
|
||||
url_expected = reverse('third_party_inactive_redirect') + '?next=' + reverse('dashboard')
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + url_expected)
|
||||
# Now we are logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 200)
|
||||
|
||||
def _check_login_page(self):
|
||||
"""
|
||||
Load the login form and check that it contains a button for the provider.
|
||||
Return the URL for logging into that provider.
|
||||
"""
|
||||
return self._check_login_or_register_page(self.login_page_url, "loginUrl")
|
||||
|
||||
def _check_register_page(self):
|
||||
"""
|
||||
Load the registration form and check that it contains a button for the provider.
|
||||
Return the URL for registering with that provider.
|
||||
"""
|
||||
return self._check_login_or_register_page(self.register_page_url, "registerUrl")
|
||||
|
||||
def _check_login_or_register_page(self, url, url_to_return):
|
||||
""" Shared logic for _check_login_page() and _check_register_page() """
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(self.PROVIDER_NAME, response.content)
|
||||
context_data = response.context['data']['third_party_auth']
|
||||
provider_urls = {provider['id']: provider[url_to_return] for provider in context_data['providers']}
|
||||
self.assertIn(self.PROVIDER_ID, provider_urls)
|
||||
return provider_urls[self.PROVIDER_ID]
|
||||
|
||||
@property
|
||||
def complete_url(self):
|
||||
""" Get the auth completion URL for this provider """
|
||||
return reverse('social:complete', kwargs={'backend': self.PROVIDER_BACKEND})
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
testutil.AUTH_FEATURES_KEY in django_settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
|
||||
@django_utils.override_settings() # For settings reversion on a method-by-method basis.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Use the 'Dummy' auth provider for generic integration tests of third_party_auth.
|
||||
"""
|
||||
import unittest
|
||||
from third_party_auth.tests import testutil
|
||||
|
||||
from .base import IntegrationTestMixin
|
||||
|
||||
|
||||
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
|
||||
class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase):
|
||||
"""
|
||||
Basic integration tests of third_party_auth using Dummy provider
|
||||
"""
|
||||
PROVIDER_ID = "oa2-dummy"
|
||||
PROVIDER_NAME = "Dummy"
|
||||
PROVIDER_BACKEND = "dummy"
|
||||
|
||||
USER_EMAIL = "adama@fleet.colonies.gov"
|
||||
USER_NAME = "William Adama"
|
||||
USER_USERNAME = "Galactica1"
|
||||
|
||||
def setUp(self):
|
||||
super(GenericIntegrationTest, self).setUp()
|
||||
self.configure_dummy_provider(enabled=True)
|
||||
|
||||
def do_provider_login(self, provider_redirect_url):
|
||||
"""
|
||||
Mock logging in to the Dummy provider
|
||||
"""
|
||||
# For the Dummy provider, the provider redirect URL is self.complete_url
|
||||
self.assertEqual(provider_redirect_url, self.url_prefix + self.complete_url)
|
||||
return self.client.get(provider_redirect_url)
|
||||
@@ -29,6 +29,8 @@ class IntegrationTestLTI(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IntegrationTestLTI, self).setUp()
|
||||
self.client.defaults['SERVER_NAME'] = 'testserver'
|
||||
self.url_prefix = 'http://testserver'
|
||||
self.configure_lti_provider(
|
||||
name='Other Tool Consumer 1', enabled=True,
|
||||
lti_consumer_key='other1',
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
"""
|
||||
Third_party_auth integration tests using a mock version of the TestShib provider
|
||||
"""
|
||||
|
||||
import json
|
||||
import unittest
|
||||
import httpretty
|
||||
from mock import patch
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from third_party_auth.tasks import fetch_saml_metadata
|
||||
from third_party_auth.tests import testutil
|
||||
from openedx.core.lib.js_utils import escape_json_dumps
|
||||
|
||||
from .base import IntegrationTestMixin
|
||||
|
||||
|
||||
TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth'
|
||||
TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml'
|
||||
TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'
|
||||
|
||||
TPA_TESTSHIB_LOGIN_URL = '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard&idp=testshib'
|
||||
TPA_TESTSHIB_REGISTER_URL = '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard&idp=testshib'
|
||||
TPA_TESTSHIB_COMPLETE_URL = '/auth/complete/tpa-saml/'
|
||||
|
||||
|
||||
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
|
||||
class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase):
|
||||
"""
|
||||
TestShib provider Integration Test, to test SAML functionality
|
||||
"""
|
||||
PROVIDER_ID = "saml-testshib"
|
||||
PROVIDER_NAME = "TestShib"
|
||||
PROVIDER_BACKEND = "tpa-saml"
|
||||
|
||||
USER_EMAIL = "myself@testshib.org"
|
||||
USER_NAME = "Me Myself And I"
|
||||
USER_USERNAME = "myself"
|
||||
|
||||
def setUp(self):
|
||||
super(TestShibIntegrationTest, self).setUp()
|
||||
self.login_page_url = reverse('signin_user')
|
||||
self.register_page_url = reverse('register_user')
|
||||
self.enable_saml(
|
||||
private_key=self._get_private_key(),
|
||||
public_key=self._get_public_key(),
|
||||
@@ -53,13 +51,14 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID')
|
||||
uid_patch.start()
|
||||
self.addCleanup(uid_patch.stop)
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
|
||||
def test_login_before_metadata_fetched(self):
|
||||
self._configure_testshib_provider(fetch_metadata=False)
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
testshib_login_url = self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
try_login_response = self.client.get(testshib_login_url)
|
||||
# The user should be redirected to back to the login page:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertEqual(try_login_response['Location'], self.url_prefix + self.login_page_url)
|
||||
@@ -68,115 +67,15 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Authentication with TestShib is currently unavailable.', response.content)
|
||||
|
||||
def test_register(self):
|
||||
self._configure_testshib_provider()
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
# The user goes to the register page, and sees a button to register with TestShib:
|
||||
self._check_register_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_REGISTER_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
testshib_response = self._fake_testshib_login_and_return()
|
||||
# We should be redirected to the register screen since this account is not linked to an edX account:
|
||||
self.assertEqual(testshib_response.status_code, 302)
|
||||
self.assertEqual(testshib_response['Location'], self.url_prefix + self.register_page_url)
|
||||
register_response = self.client.get(self.register_page_url)
|
||||
# We'd now like to see if the "You've successfully signed into TestShib" message is
|
||||
# shown, but it's managed by a JavaScript runtime template, and we can't run JS in this
|
||||
# type of test, so we just check for the variable that triggers that message.
|
||||
self.assertIn('"currentProvider": "TestShib"', register_response.content)
|
||||
self.assertIn('"errorMessage": null', register_response.content)
|
||||
# Now do a crude check that the data (e.g. email) from the provider is displayed in the form:
|
||||
self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content)
|
||||
self.assertIn('"defaultValue": "Me Myself And I"', register_response.content)
|
||||
# Now complete the form:
|
||||
ajax_register_response = self.client.post(
|
||||
reverse('user_api_registration'),
|
||||
{
|
||||
'email': 'myself@testshib.org',
|
||||
'name': 'Myself',
|
||||
'username': 'myself',
|
||||
'honor_code': True,
|
||||
}
|
||||
)
|
||||
self.assertEqual(ajax_register_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self.verify_user_email('myself@testshib.org')
|
||||
self._test_return_login()
|
||||
|
||||
def test_login(self):
|
||||
""" Configure TestShib before running the login test """
|
||||
self._configure_testshib_provider()
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
user = UserFactory.create()
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
testshib_response = self._fake_testshib_login_and_return()
|
||||
# We should be redirected to the login screen since this account is not linked to an edX account:
|
||||
self.assertEqual(testshib_response.status_code, 302)
|
||||
self.assertEqual(testshib_response['Location'], self.url_prefix + self.login_page_url)
|
||||
login_response = self.client.get(self.login_page_url)
|
||||
# We'd now like to see if the "You've successfully signed into TestShib" message is
|
||||
# shown, but it's managed by a JavaScript runtime template, and we can't run JS in this
|
||||
# type of test, so we just check for the variable that triggers that message.
|
||||
self.assertIn('"currentProvider": "TestShib"', login_response.content)
|
||||
self.assertIn('"errorMessage": null', login_response.content)
|
||||
# Now the user enters their username and password.
|
||||
# The AJAX on the page will log them in:
|
||||
ajax_login_response = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
{'email': user.email, 'password': 'test'}
|
||||
)
|
||||
self.assertEqual(ajax_login_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
super(TestShibIntegrationTest, self).test_login()
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self._test_return_login()
|
||||
|
||||
def _test_return_login(self):
|
||||
""" Test logging in to an account that is already linked. """
|
||||
# Make sure we're not logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 302)
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
login_response = self._fake_testshib_login_and_return()
|
||||
# There will be one weird redirect required to set the login cookie:
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And then we should be redirected to the dashboard:
|
||||
login_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
# Now we are logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 200)
|
||||
def test_register(self):
|
||||
""" Configure TestShib before running the register test """
|
||||
self._configure_testshib_provider()
|
||||
super(TestShibIntegrationTest, self).test_register()
|
||||
|
||||
def _freeze_time(self, timestamp):
|
||||
""" Mock the current time for SAML, so we can replay canned requests/responses """
|
||||
@@ -184,22 +83,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
now_patch.start()
|
||||
self.addCleanup(now_patch.stop)
|
||||
|
||||
def _check_login_page(self):
|
||||
""" Load the login form and check that it contains a TestShib button """
|
||||
response = self.client.get(self.login_page_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("TestShib", response.content)
|
||||
self.assertIn(escape_json_dumps(TPA_TESTSHIB_LOGIN_URL), response.content)
|
||||
return response
|
||||
|
||||
def _check_register_page(self):
|
||||
""" Load the login form and check that it contains a TestShib button """
|
||||
response = self.client.get(self.register_page_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("TestShib", response.content)
|
||||
self.assertIn(escape_json_dumps(TPA_TESTSHIB_REGISTER_URL), response.content)
|
||||
return response
|
||||
|
||||
def _configure_testshib_provider(self, **kwargs):
|
||||
""" Enable and configure the TestShib SAML IdP as a third_party_auth provider """
|
||||
fetch_metadata = kwargs.pop('fetch_metadata', True)
|
||||
@@ -219,11 +102,12 @@ class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
self.assertEqual(num_changed, 1)
|
||||
self.assertEqual(num_total, 1)
|
||||
|
||||
def _fake_testshib_login_and_return(self):
|
||||
def do_provider_login(self, provider_redirect_url):
|
||||
""" Mocked: the user logs in to TestShib and then gets redirected back """
|
||||
# The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response:
|
||||
self.assertTrue(provider_redirect_url.startswith(TESTSHIB_SSO_URL))
|
||||
return self.client.post(
|
||||
TPA_TESTSHIB_COMPLETE_URL,
|
||||
self.complete_url,
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
data=self.read_data_file('testshib_response.txt'),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Test the views served by third_party_auth.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
import ddt
|
||||
from lxml import etree
|
||||
from onelogin.saml2.errors import OneLogin_Saml2_Error
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.contrib.auth.models import User
|
||||
from provider.oauth2.models import Client as OAuth2Client
|
||||
from provider import constants
|
||||
import django.test
|
||||
from mako.template import Template
|
||||
import mock
|
||||
import os.path
|
||||
|
||||
@@ -27,6 +28,18 @@ AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH'
|
||||
AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES
|
||||
|
||||
|
||||
def patch_mako_templates():
|
||||
""" Patch mako so the django test client can access template context """
|
||||
orig_render = Template.render_unicode
|
||||
|
||||
def wrapped_render(*args, **kwargs):
|
||||
""" Render the template and send the context info to any listeners that want it """
|
||||
django.test.signals.template_rendered.send(sender=None, template=None, context=kwargs)
|
||||
return orig_render(*args, **kwargs)
|
||||
|
||||
return mock.patch.multiple(Template, render_unicode=wrapped_render, render=wrapped_render)
|
||||
|
||||
|
||||
class FakeDjangoSettings(object):
|
||||
"""A fake for Django settings."""
|
||||
|
||||
@@ -109,6 +122,13 @@ class ThirdPartyAuthTestMixin(object):
|
||||
kwargs.setdefault("secret", "test")
|
||||
return cls.configure_oauth_provider(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def configure_dummy_provider(cls, **kwargs):
|
||||
""" Update the settings for the Twitter third party auth provider/backend """
|
||||
kwargs.setdefault("name", "Dummy")
|
||||
kwargs.setdefault("backend_name", "dummy")
|
||||
return cls.configure_oauth_provider(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def verify_user_email(cls, email):
|
||||
""" Mark the user with the given email as verified """
|
||||
@@ -135,19 +155,18 @@ class ThirdPartyAuthTestMixin(object):
|
||||
|
||||
class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase):
|
||||
"""Base class for auth test cases."""
|
||||
pass
|
||||
def setUp(self):
|
||||
super(TestCase, self).setUp()
|
||||
# Explicitly set a server name that is compatible with all our providers:
|
||||
# (The SAML lib we use doesn't like the default 'testserver' as a domain)
|
||||
self.client.defaults['SERVER_NAME'] = 'example.none'
|
||||
self.url_prefix = 'http://example.none'
|
||||
|
||||
|
||||
class SAMLTestCase(TestCase):
|
||||
"""
|
||||
Base class for SAML-related third_party_auth tests
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(SAMLTestCase, self).setUp()
|
||||
self.client.defaults['SERVER_NAME'] = 'example.none' # The SAML lib we use doesn't like testserver' as a domain
|
||||
self.url_prefix = 'http://example.none'
|
||||
|
||||
@classmethod
|
||||
def _get_public_key(cls, key_name='saml_key'):
|
||||
""" Get a public key for use in the test. """
|
||||
|
||||
@@ -6,7 +6,7 @@ from .views import inactive_user_view, saml_metadata_view, lti_login_and_complet
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^auth/inactive', inactive_user_view),
|
||||
url(r'^auth/inactive', inactive_user_view, name="third_party_inactive_redirect"),
|
||||
url(r'^auth/saml/metadata.xml', saml_metadata_view),
|
||||
url(r'^auth/login/(?P<backend>lti)/$', lti_login_and_complete_view),
|
||||
url(r'^auth/', include('social.apps.django_app.urls', namespace='social')),
|
||||
|
||||
@@ -17,8 +17,13 @@ URL_NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'socia
|
||||
|
||||
def inactive_user_view(request):
|
||||
"""
|
||||
A newly registered user has completed the social auth pipeline.
|
||||
Their account is not yet activated, but we let them login this once.
|
||||
A newly or recently registered user has completed the social auth pipeline.
|
||||
Their account is not yet activated, but we let them login since the third party auth
|
||||
provider is trusted to vouch for them. See details in pipeline.py.
|
||||
|
||||
The reason this view exists is that if we don't define this as the
|
||||
SOCIAL_AUTH_INACTIVE_USER_URL, inactive users will get sent to LOGIN_ERROR_URL, which we
|
||||
don't want.
|
||||
"""
|
||||
# 'next' may be set to '/account/finish_auth/.../' if this user needs to be auto-enrolled
|
||||
# in a course. Otherwise, just redirect them to the dashboard, which displays a message
|
||||
|
||||
@@ -11,7 +11,6 @@ from __future__ import absolute_import
|
||||
import abc
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
class BaseBackend(object):
|
||||
"""
|
||||
Abstract Base Class for event tracking backends.
|
||||
|
||||
@@ -113,6 +113,5 @@ class DummyBackend(BaseBackend):
|
||||
self.flag = options.get('flag', False)
|
||||
self.count = 0
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def send(self, event):
|
||||
self.count += 1
|
||||
|
||||
@@ -164,7 +164,6 @@ class OuterAtomic(transaction.Atomic):
|
||||
# The inner atomic starts a savepoint around the test.
|
||||
# So, for tests only, there should be exactly one savepoint_id and two atomic_for_testcase_calls.
|
||||
# atomic_for_testcase_calls below is added in a monkey-patch for tests only.
|
||||
# pylint: disable=no-member
|
||||
if self.ALLOW_NESTED and (self.atomic_for_testcase_calls - len(connection.savepoint_ids)) < 1:
|
||||
raise transaction.TransactionManagementError('Cannot be inside an atomic block.')
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# pylint: disable=invalid-name
|
||||
"""
|
||||
Utility library for working with the edx-organizations app
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# pylint: disable=no-member
|
||||
"""
|
||||
This file exposes a number of password complexity validators which can be optionally added to
|
||||
account creation
|
||||
@@ -7,7 +6,7 @@ This file was inspired by the django-passwords project at https://github.com/dst
|
||||
authored by dstufft (https://github.com/dstufft)
|
||||
"""
|
||||
from __future__ import division
|
||||
import string # pylint: disable=deprecated-module
|
||||
import string
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
Utility Mixins for unit tests
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from mock import patch
|
||||
@@ -95,6 +100,19 @@ class EventTestMixin(object):
|
||||
self.mock_tracker.reset_mock()
|
||||
|
||||
|
||||
class PatchMediaTypeMixin(object):
|
||||
"""
|
||||
Generic mixin for verifying unsupported media type in PATCH
|
||||
"""
|
||||
def test_patch_unsupported_media_type(self):
|
||||
response = self.client.patch(
|
||||
self.url,
|
||||
json.dumps({}),
|
||||
content_type=self.unsupported_media_type
|
||||
)
|
||||
self.assertEqual(response.status_code, 415)
|
||||
|
||||
|
||||
def patch_testcase():
|
||||
"""
|
||||
Disable commit_on_success decorators for tests in TestCase subclasses.
|
||||
|
||||
@@ -18,5 +18,5 @@ def reload_django_url_config():
|
||||
if urlconf and urlconf in sys.modules:
|
||||
reload(sys.modules[urlconf])
|
||||
reloaded = import_module(urlconf)
|
||||
reloaded_urls = getattr(reloaded, 'urlpatterns')
|
||||
reloaded_urls = reloaded.urlpatterns
|
||||
set_urlconf(tuple(reloaded_urls))
|
||||
|
||||
@@ -56,13 +56,18 @@ def jsonable_server_error(request, template_name='500.html'):
|
||||
return server_error(request, template_name=template_name)
|
||||
|
||||
|
||||
def handle_500(template_path, context=None):
|
||||
def handle_500(template_path, context=None, test_func=None):
|
||||
"""
|
||||
Decorator for view specific 500 error handling.
|
||||
Custom handling will be skipped only if test_func is passed and it returns False
|
||||
|
||||
Usage::
|
||||
Usage:
|
||||
|
||||
@handle_500(template_path='certificates/server-error.html', context={'error-info': 'Internal Server Error'})
|
||||
@handle_500(
|
||||
template_path='certificates/server-error.html',
|
||||
context={'error-info': 'Internal Server Error'},
|
||||
test_func=lambda request: request.GET.get('preview', None)
|
||||
)
|
||||
def my_view(request):
|
||||
# Any unhandled exception in this view would be handled by the handle_500 decorator
|
||||
# ...
|
||||
@@ -83,9 +88,15 @@ def handle_500(template_path, context=None):
|
||||
if settings.DEBUG:
|
||||
# In debug mode let django process the 500 errors and display debug info for the developer
|
||||
raise
|
||||
else:
|
||||
elif test_func is None or test_func(request):
|
||||
# Display custom 500 page if either
|
||||
# 1. test_func is None (meaning nothing to test)
|
||||
# 2. or test_func(request) returns True
|
||||
log.exception("Error in django view.")
|
||||
return render_to_response(template_path, context)
|
||||
else:
|
||||
# Do not show custom 500 error when test fails
|
||||
raise
|
||||
return inner
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class XBlockDisableConfig(ConfigurationModel):
|
||||
if not config.enabled:
|
||||
return False
|
||||
|
||||
return block_type in config.disabled_blocks.split() # pylint: disable=no-member
|
||||
return block_type in config.disabled_blocks.split()
|
||||
|
||||
@classmethod
|
||||
def disabled_block_types(cls):
|
||||
@@ -39,4 +39,4 @@ class XBlockDisableConfig(ConfigurationModel):
|
||||
if not config.enabled:
|
||||
return ()
|
||||
|
||||
return config.disabled_blocks.split() # pylint: disable=no-member
|
||||
return config.disabled_blocks.split()
|
||||
|
||||
@@ -59,7 +59,10 @@ class DjangoXBlockUserService(UserService):
|
||||
|
||||
if django_user is not None and django_user.is_authenticated():
|
||||
# This full_name is dependent on edx-platform's profile implementation
|
||||
full_name = getattr(django_user.profile, 'name') if hasattr(django_user, 'profile') else None
|
||||
if hasattr(django_user, 'profile'):
|
||||
full_name = django_user.profile.name
|
||||
else:
|
||||
full_name = None
|
||||
xblock_user.full_name = full_name
|
||||
xblock_user.emails = [django_user.email]
|
||||
xblock_user.opt_attrs[ATTR_KEY_IS_AUTHENTICATED] = True
|
||||
|
||||
@@ -152,7 +152,7 @@ class ClarificationRenderer(object):
|
||||
self.system = system
|
||||
# Get any text content found inside this tag prior to the first child tag. It may be a string or None type.
|
||||
initial_text = xml.text if xml.text else ''
|
||||
self.inner_html = initial_text + ''.join(etree.tostring(element) for element in xml) # pylint: disable=no-member
|
||||
self.inner_html = initial_text + ''.join(etree.tostring(element) for element in xml)
|
||||
self.tail = xml.tail
|
||||
|
||||
def get_html(self):
|
||||
@@ -161,7 +161,7 @@ class ClarificationRenderer(object):
|
||||
"""
|
||||
context = {'clarification': self.inner_html}
|
||||
html = self.system.render_template("clarification.html", context)
|
||||
xml = etree.XML(html) # pylint: disable=no-member
|
||||
xml = etree.XML(html)
|
||||
# We must include any text that was following our original <clarification>...</clarification> XML node.:
|
||||
xml.tail = self.tail
|
||||
return xml
|
||||
|
||||
@@ -467,7 +467,7 @@ class ChoiceGroup(InputTypeBase):
|
||||
raise Exception(msg)
|
||||
|
||||
self.choices = self.extract_choices(self.xml, i18n)
|
||||
self._choices_map = dict(self.choices,) # pylint: disable=attribute-defined-outside-init
|
||||
self._choices_map = dict(self.choices,)
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
|
||||
@@ -55,7 +55,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
registry = TagRegistry()
|
||||
|
||||
CorrectMap = correctmap.CorrectMap # pylint: disable=invalid-name
|
||||
CorrectMap = correctmap.CorrectMap
|
||||
CORRECTMAP_PY = None
|
||||
|
||||
# Make '_' a no-op so we can scrape strings. Using lambda instead of
|
||||
@@ -2666,7 +2666,7 @@ class SymbolicResponse(CustomResponse):
|
||||
## score: Points to be assigned (numeric, can be float)
|
||||
## msg: Message from grader to display to student (string)
|
||||
|
||||
ScoreMessage = namedtuple('ScoreMessage', ['valid', 'correct', 'points', 'msg']) # pylint: disable=invalid-name
|
||||
ScoreMessage = namedtuple('ScoreMessage', ['valid', 'correct', 'points', 'msg'])
|
||||
|
||||
|
||||
@registry.register
|
||||
@@ -3055,7 +3055,7 @@ class ExternalResponse(LoncapaResponse):
|
||||
cmap = CorrectMap()
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
log.error(
|
||||
'Error %s: cannot get student answer for %s; student_answers=%s',
|
||||
err,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user