diff --git a/common/djangoapps/third_party_auth/__init__.py b/common/djangoapps/third_party_auth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..1f9c56906c192f257f6156493b8c4f11b0bfdd7e --- /dev/null +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -0,0 +1,9 @@ +"""Auth pipeline definitions.""" + +from social.pipeline import partial + + +@partial.partial +def step(*args, **kwargs): + """Fake pipeline step; just throws loudly for now.""" + raise NotImplementedError('%s, %s' % (args, kwargs)) diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py new file mode 100644 index 0000000000000000000000000000000000000000..1b1e17796d436fe28f524ca79c38072de5e56126 --- /dev/null +++ b/common/djangoapps/third_party_auth/provider.py @@ -0,0 +1,125 @@ +"""Third-party auth provider definitions. + +Loaded by Django's settings mechanism. Consequently, this module must not +invoke the Django armature. +""" + + +class BaseProvider(object): + """Abstract base class for third-party auth providers. + + All providers must subclass BaseProvider -- otherwise, they cannot be put + in the provider Registry. + """ + + # String. Dot-delimited module.Class. The name of the backend + # implementation to load. + AUTHENTICATION_BACKEND = None + # String. User-facing name of the provider. Must be unique across all + # enabled providers. + NAME = None + + # Dict of string -> object. Settings that will be merged into Django's + # settings instance. In most cases the value will be None, since real + # values are merged from .json files (foo.auth.json; foo.env.json) onto the + # settings instance during application initialization. + SETTINGS = {} + + @classmethod + def merge_onto(cls, settings): + """Merge class-level settings onto a django `settings` module.""" + for key, value in cls.SETTINGS.iteritems(): + setattr(settings, key, value) + + +class GoogleOauth2(BaseProvider): + """Provider for Google's Oauth2 auth system.""" + + AUTHENTICATION_BACKEND = 'social.backends.google.GoogleOAuth2' + NAME = 'Google' + SETTINGS = { + 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': None, + 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': None, + } + + +class LinkedInOauth2(BaseProvider): + """Provider for LinkedIn's Oauth2 auth system.""" + + AUTHENTICATION_BACKEND = 'social.backends.linkedin.LinkedinOAuth2' + NAME = 'LinkedIn' + SETTINGS = { + 'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': None, + 'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': None, + } + + +class MozillaPersona(BaseProvider): + """Provider for Mozilla's Persona auth system.""" + + AUTHENTICATION_BACKEND = 'social.backends.persona.PersonaAuth' + NAME = 'Mozilla Persona' + + +class Registry(object): + """Singleton registry of third-party auth providers. + + Providers must subclass BaseProvider in order to be usable in the registry. + """ + + _CONFIGURED = False + _ENABLED = {} + + @classmethod + def _check_configured(cls): + """Ensures registry is configured.""" + if not cls._CONFIGURED: + raise RuntimeError('Registry not configured') + + @classmethod + def _get_all(cls): + """Gets all provider implementations loaded into the Python runtime.""" + # BaseProvider does so have __subclassess__. pylint: disable-msg=no-member + return {klass.NAME: klass for klass in BaseProvider.__subclasses__()} + + @classmethod + def _enable(cls, provider): + """Enables a single `provider`.""" + if provider.NAME in cls._ENABLED: + raise ValueError('Provider %s already enabled' % provider.NAME) + cls._ENABLED[provider.NAME] = provider + + @classmethod + def configure_once(cls, provider_names): + """Configures providers. + + Takes `provider_names`, a list of string. + """ + if cls._CONFIGURED: + raise ValueError('Provider registry already configured') + # Flip the bit eagerly -- configure() should not be re-callable if one + # _enable call fails. + cls._CONFIGURED = True + for name in provider_names: + all_providers = cls._get_all() + if name not in all_providers: + raise ValueError('No implementation found for provider ' + name) + cls._enable(all_providers.get(name)) + + @classmethod + def enabled(cls): + """Returns list of enabled providers.""" + cls._check_configured() + return sorted(cls._ENABLED.values(), key=lambda provider: provider.NAME) + + @classmethod + def get(cls, provider_name): + """Gets provider named `provider_name` string if enabled, else None.""" + cls._check_configured() + return cls._ENABLED.get(provider_name) + + @classmethod + def _reset(cls): + """Returns the registry to an unconfigured state; for tests only.""" + cls._CONFIGURED = False + cls._ENABLED = {} diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..0ff55e1213d8b9eb27a184ba9aae0ec49603a00a --- /dev/null +++ b/common/djangoapps/third_party_auth/settings.py @@ -0,0 +1,106 @@ +"""Settings for the third-party auth module. + +Defers configuration of settings so we can inspect the provider registry and +create settings placeholders for only those values actually needed by a given +deployment. Required by Django; consequently, this file must not invoke the +Django armature. + +The flow for settings registration is: + +The base settings file contains a boolean, ENABLE_THIRD_PARTY_AUTH, indicating +whether this module is enabled. Ancillary settings files (aws.py, dev.py) put +options in THIRD_PARTY_SETTINGS. startup.py probes the ENABLE_THIRD_PARTY_AUTH. +If true, it: + + a) loads this module. + b) calls apply_settings(), passing in settings.THIRD_PARTY_AUTH. + THIRD_PARTY AUTH is a dict of the form + + 'THIRD_PARTY_AUTH': { + '<PROVIDER_NAME>': { + '<PROVIDER_SETTING_NAME>': '<PROVIDER_SETTING_VALUE>', + [...] + }, + [...] + } + + If you are using a dev settings file, your settings dict starts at the + level of <PROVIDER_NAME> and is a map of provider name string to + settings dict. If you are using an auth.json file, it should contain a + THIRD_PARTY_AUTH entry as above. + c) apply_settings() builds a list of <PROVIDER_NAMES>. These are the + enabled third party auth providers for the deployment. These are enabled + in provider.Registry, the canonical list of enabled providers. + d) then, it sets global, provider-independent settings. + e) then, it sets provider-specific settings. For each enabled provider, we + read its SETTINGS member. These are merged onto the Django settings + object. In most cases these are stubs and the real values are set from + THIRD_PARTY_AUTH. All values that are set from this dict must first be + initialized from SETTINGS. This allows us to validate the dict and + ensure that the values match expected configuration options on the + provider. + f) finally, the (key, value) pairs from the dict file are merged onto the + django settings object. +""" + +from . import provider + + +def _merge_auth_info(django_settings, auth_info): + """Merge `auth_info` dict onto `django_settings` module.""" + enabled_provider_names = [] + to_merge = [] + + for provider_name, provider_dict in auth_info.items(): + enabled_provider_names.append(provider_name) + # Merge iff all settings have been intialized. + for key in provider_dict: + if key not in dir(django_settings): + raise ValueError('Auth setting %s not initialized' % key) + to_merge.append(provider_dict) + + for passed_validation in to_merge: + for key, value in passed_validation.iteritems(): + setattr(django_settings, key, value) + + +def _set_global_settings(django_settings): + """Set provider-independent settings.""" + # Register and configure python-social-auth with Django. + django_settings.INSTALLED_APPS += ( + 'social.apps.django_app.default', + 'third_party_auth', + ) + django_settings.TEMPLATE_CONTEXT_PROCESSORS += ( + 'social.apps.django_app.context_processors.backends', + 'social.apps.django_app.context_processors.login_redirect', + ) + # Inject our customized auth pipeline. All auth backends must work with + # this pipeline. + django_settings.SOCIAL_AUTH_PIPELINE = ( + 'third_party_auth.pipeline.step', + ) + + +def _set_provider_settings(django_settings, enabled_providers, auth_info): + """Set provider-specific settings.""" + # Must prepend here so we get called first. + django_settings.AUTHENTICATION_BACKENDS = ( + tuple(enabled_provider.AUTHENTICATION_BACKEND for enabled_provider in enabled_providers) + + django_settings.AUTHENTICATION_BACKENDS) + + # Merge settings from provider classes, and configure all placeholders. + for enabled_provider in enabled_providers: + enabled_provider.merge_onto(django_settings) + + # Merge settings from <deployment>.auth.json. + _merge_auth_info(django_settings, auth_info) + + +def apply_settings(auth_info, django_settings): + """Apply settings from `auth_info` dict to `django_settings` module.""" + provider_names = auth_info.keys() + provider.Registry.configure_once(provider_names) + enabled_providers = provider.Registry.enabled() + _set_global_settings(django_settings) + _set_provider_settings(django_settings, enabled_providers, auth_info) diff --git a/common/djangoapps/third_party_auth/tests/__init__.py b/common/djangoapps/third_party_auth/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/djangoapps/third_party_auth/tests/test_provider.py b/common/djangoapps/third_party_auth/tests/test_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..18774eb61a7404b673a214b3bd0bd203af93510f --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_provider.py @@ -0,0 +1,71 @@ +""" +Test configuration of providers. +""" + +from third_party_auth import provider +from third_party_auth.tests import testutil + + +class RegistryTest(testutil.TestCase): + """Tests registry discovery and operation.""" + + # Allow access to protected methods (or module-protected methods) under + # test. + # pylint: disable-msg=protected-access + + def test_calling_configure_once_twice_raises_value_error(self): + provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + + with self.assertRaisesRegexp(ValueError, '^.*already configured$'): + provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + + def test_configure_once_adds_gettable_providers(self): + provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME)) + + def test_configuring_provider_with_no_implementation_raises_value_error(self): + with self.assertRaisesRegexp(ValueError, '^.*no_implementation$'): + provider.Registry.configure_once(['no_implementation']) + + def test_configuring_single_provider_twice_raises_value_error(self): + provider.Registry._enable(provider.GoogleOauth2) + + with self.assertRaisesRegexp(ValueError, '^.*already enabled'): + provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + + def test_custom_provider_can_be_enabled(self): + name = 'CustomProvider' + + with self.assertRaisesRegexp(ValueError, '^No implementation.*$'): + provider.Registry.configure_once([name]) + + class CustomProvider(provider.BaseProvider): + """Custom class to ensure BaseProvider children outside provider can be enabled.""" + + NAME = name + + provider.Registry._reset() + provider.Registry.configure_once([CustomProvider.NAME]) + self.assertEqual([CustomProvider], provider.Registry.enabled()) + + def test_enabled_raises_runtime_error_if_not_configured(self): + with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'): + provider.Registry.enabled() + + def test_enabled_returns_list_of_enabled_providers_sorted_by_name(self): + all_providers = provider.Registry._get_all() + provider.Registry.configure_once(all_providers.keys()) + self.assertEqual( + sorted(all_providers.values(), key=lambda provider: provider.NAME), provider.Registry.enabled()) + + def test_get_raises_runtime_error_if_not_configured(self): + with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'): + provider.Registry.get('anything') + + def test_get_returns_enabled_provider(self): + provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME)) + + def test_get_returns_none_if_provider_not_enabled(self): + provider.Registry.configure_once([]) + self.assertIsNone(provider.Registry.get(provider.MozillaPersona.NAME)) diff --git a/common/djangoapps/third_party_auth/tests/test_settings.py b/common/djangoapps/third_party_auth/tests/test_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..05f502928430d94aa2bbd5f7cfb5881894f018e6 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_settings.py @@ -0,0 +1,68 @@ +""" +Unit tests for settings code. +""" + +from third_party_auth import provider +from third_party_auth import settings +from third_party_auth.tests import testutil + + +_ORIGINAL_AUTHENTICATION_BACKENDS = ('first_authentication_backend',) +_ORIGINAL_INSTALLED_APPS = ('first_installed_app',) +_ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS = ('first_template_context_preprocessor',) +_SETTINGS_MAP = { + 'AUTHENTICATION_BACKENDS': _ORIGINAL_AUTHENTICATION_BACKENDS, + 'INSTALLED_APPS': _ORIGINAL_INSTALLED_APPS, + 'TEMPLATE_CONTEXT_PROCESSORS': _ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS, +} + + +class SettingsUnitTest(testutil.TestCase): + """Unit tests for settings management code.""" + + # Suppress sprurious no-member warning on fakes. + # pylint: disable-msg=no-member + + def setUp(self): + super(SettingsUnitTest, self).setUp() + self.settings = testutil.FakeDjangoSettings(_SETTINGS_MAP) + + def test_apply_settings_adds_third_party_auth_to_installed_apps(self): + settings.apply_settings({}, self.settings) + self.assertIn('third_party_auth', self.settings.INSTALLED_APPS) + + def test_apply_settings_enables_no_providers_and_completes_when_app_info_empty(self): + settings.apply_settings({}, self.settings) + self.assertEqual([], provider.Registry.enabled()) + + def test_apply_settings_initializes_stubs_and_merges_settings_from_auth_info(self): + for key in provider.GoogleOauth2.SETTINGS: + self.assertFalse(hasattr(self.settings, key)) + + auth_info = { + provider.GoogleOauth2.NAME: { + 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_oauth2_key', + }, + } + settings.apply_settings(auth_info, self.settings) + self.assertEqual('google_oauth2_key', self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY) + self.assertIsNone(self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET) + + def test_apply_settings_prepends_auth_backends(self): + self.assertEqual(_ORIGINAL_AUTHENTICATION_BACKENDS, self.settings.AUTHENTICATION_BACKENDS) + settings.apply_settings({provider.GoogleOauth2.NAME: {}, provider.MozillaPersona.NAME: {}}, self.settings) + self.assertEqual(( + provider.GoogleOauth2.AUTHENTICATION_BACKEND, provider.MozillaPersona.AUTHENTICATION_BACKEND) + + _ORIGINAL_AUTHENTICATION_BACKENDS, + self.settings.AUTHENTICATION_BACKENDS) + + def test_apply_settings_raises_value_error_if_provider_contains_uninitialized_setting(self): + bad_setting_name = 'bad_setting' + self.assertNotIn('bad_setting_name', provider.GoogleOauth2.SETTINGS) + auth_info = { + provider.GoogleOauth2.NAME: { + bad_setting_name: None, + }, + } + with self.assertRaisesRegexp(ValueError, '^.*not initialized$'): + settings.apply_settings(auth_info, self.settings) diff --git a/common/djangoapps/third_party_auth/tests/test_settings_integration.py b/common/djangoapps/third_party_auth/tests/test_settings_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..15cff5605a18f0843d79547cadbb4a652621759f --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_settings_integration.py @@ -0,0 +1,39 @@ +""" +Integration tests for settings code. +""" + +import mock +import unittest + +from django.conf import settings + +from third_party_auth import provider +from third_party_auth import settings as auth_settings +from third_party_auth.tests import testutil + +_AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' + + +class SettingsIntegrationTest(testutil.TestCase): + """Integration tests of auth settings pipeline.""" + + @unittest.skipUnless(_AUTH_FEATURES_KEY in settings.FEATURES, _AUTH_FEATURES_KEY + ' not in settings.FEATURES') + def test_enable_third_party_auth_is_disabled_by_default(self): + self.assertIs(False, settings.FEATURES.get(_AUTH_FEATURES_KEY)) + + @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_AUTH': True}) + def test_can_enable_google_oauth2(self): + auth_settings.apply_settings({'Google': {'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_key'}}, settings) + self.assertEqual([provider.GoogleOauth2], provider.Registry.enabled()) + self.assertEqual('google_key', settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY) + + @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_AUTH': True}) + def test_can_enable_linkedin_oauth2(self): + auth_settings.apply_settings({'LinkedIn': {'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': 'linkedin_key'}}, settings) + self.assertEqual([provider.LinkedInOauth2], provider.Registry.enabled()) + self.assertEqual('linkedin_key', settings.SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY) + + @mock.patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_ATUH': True}) + def test_can_enable_mozilla_persona(self): + auth_settings.apply_settings({'Mozilla Persona': {}}, settings) + self.assertEqual([provider.MozillaPersona], provider.Registry.enabled()) diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py new file mode 100644 index 0000000000000000000000000000000000000000..a431aaca02fd41425f1bcefc251dd2a6ae7ba998 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -0,0 +1,34 @@ +""" +Utilities for writing third_party_auth tests. + +Used by Django and non-Django tests; must not have Django deps. +""" + +import unittest + +from third_party_auth import provider + + +class FakeDjangoSettings(object): + """A fake for Django settings.""" + + def __init__(self, mappings): + """Initializes the fake from `mappings`, a dict.""" + for key, value in mappings.iteritems(): + setattr(self, key, value) + + +class TestCase(unittest.TestCase): + """Base class for auth test cases.""" + + # Allow access to protected methods (or module-protected methods) under + # test. + # pylint: disable-msg=protected-access + + def setUp(self): + super(TestCase, self).setUp() + provider.Registry._reset() + + def tearDown(self): + provider.Registry._reset() + super(TestCase, self).tearDown() diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..05322a680d2a08fe7cefe3c1c76114111fdb104c --- /dev/null +++ b/common/djangoapps/third_party_auth/urls.py @@ -0,0 +1,7 @@ +"""Url configuration for the auth module.""" + +from django.conf.urls import include, patterns, url + +urlpatterns = patterns( + '', url(r'^auth/', include('social.apps.django_app.urls', namespace='social')), +) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 373704b5e26fa90b3f86e36b36581edf8e59077e..dc8a726d06807ab621916e2eb4b5d8897addfc28 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -385,3 +385,6 @@ TIME_ZONE_DISPLAYED_FOR_DEADLINES = ENV_TOKENS.get("TIME_ZONE_DISPLAYED_FOR_DEAD ##### X-Frame-Options response header settings ##### X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) + +##### Third-party auth options ################################################ +THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH) diff --git a/lms/envs/common.py b/lms/envs/common.py index 197e677b5f4be99778eb196969c2f14e66a2cb64..35e5a39f68140974656ec2e702c563f911c68f54 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -234,6 +234,10 @@ FEATURES = { # Turn on/off Microsites feature 'USE_MICROSITES': False, + + # Turn on third-party auth. Disabled for now because full implementations are not yet available. Remember to syncdb + # if you enable this; we don't create tables by default. + 'ENABLE_THIRD_PARTY_AUTH': False, } # Used for A/B testing @@ -1247,6 +1251,7 @@ LINKEDIN_API = { 'COMPANY_ID': '2746406', } + ##### ACCOUNT LOCKOUT DEFAULT PARAMETERS ##### MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = 5 MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60 @@ -1465,3 +1470,7 @@ for app_name in OPTIONAL_APPS: except ImportError: continue INSTALLED_APPS += (app_name,) + +# Stub for third_party_auth options. +# See common/djangoapps/third_party_auth/settings.py for configuration details. +THIRD_PARTY_AUTH = {} diff --git a/lms/startup.py b/lms/startup.py index 1c8cdc8ef74a6ef5610627e235691f635c9b191d..462fb3f8fd40a59a674337969a624f9b577a8098 100644 --- a/lms/startup.py +++ b/lms/startup.py @@ -26,6 +26,9 @@ def run(): if settings.FEATURES.get('USE_MICROSITES', False): enable_microsites() + if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False): + enable_third_party_auth() + def enable_theme(): """ @@ -99,3 +102,14 @@ def enable_microsites(): edxmako.startup.run() settings.STATICFILES_DIRS.insert(0, microsites_root) + + +def enable_third_party_auth(): + """ + Enable the use of third_party_auth, which allows users to sign in to edX + using other identity providers. For configuration details, see + common/djangoapps/third_party_auth/settings.py. + """ + + from third_party_auth import settings as auth_settings + auth_settings.apply_settings(settings.THIRD_PARTY_AUTH, settings) diff --git a/lms/urls.py b/lms/urls.py index 4a45c1df97eaef433d31a4bf2c9d9d3a4deb121f..6823bc11bc121c94fd6f7c2ee26bbccd5d6f7037 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -488,6 +488,12 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'): url(r'^auto_auth$', 'student.views.auto_auth'), ) +# Third-party auth. +if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): + urlpatterns += ( + url(r'', include('third_party_auth.urls')), + ) + urlpatterns = patterns(*urlpatterns) if settings.DEBUG: diff --git a/pylintrc b/pylintrc index aacfc999ff9548d62ab0096f106db365ef65eb53..21966c7f8ac188f3c3c74ece55c432c498e0776f 100644 --- a/pylintrc +++ b/pylintrc @@ -153,10 +153,10 @@ const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ +function-rgx=([a-z_][a-z0-9_]{2,30}|test_[a-z0-9_]+)$ # Regular expression which should only match correct method names -method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff)$ +method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 115112032144c6466abb24864a2d0960a700a06e..d649bd6658a0fae883fa3dc302ec049d0d5ae7e4 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -60,6 +60,7 @@ pyparsing==1.5.6 python-memcached==1.48 python-openid==2.2.5 python-dateutil==2.1 +python-social-auth==0.1.21 pytz==2012h pysrt==0.4.7 PyYAML==3.10