From 881e3ba564a0452ff04f46b6a29f7ad53e25fb6b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 23 Jan 2014 11:24:42 -0500 Subject: [PATCH 1/3] Add the ability to dark-launch languages To mark a language as dark-launched, add it to the DARK_LANGUAGES django conf setting. To activate a dark-launched language, set he query parameter `preview-lang` to the language code on any url. [LMS-2045] [LMS-2077] [LMS-2076] --- CHANGELOG.rst | 4 + cms/envs/aws.py | 6 + cms/envs/common.py | 19 ++- cms/envs/dev.py | 5 - common/djangoapps/dark_lang/README.rst | 16 +++ common/djangoapps/dark_lang/__init__.py | 0 common/djangoapps/dark_lang/middleware.py | 87 ++++++++++++ common/djangoapps/dark_lang/tests.py | 155 ++++++++++++++++++++++ lms/envs/aws.py | 1 + lms/envs/common.py | 20 ++- lms/envs/dev.py | 5 - 11 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 common/djangoapps/dark_lang/README.rst create mode 100644 common/djangoapps/dark_lang/__init__.py create mode 100644 common/djangoapps/dark_lang/middleware.py create mode 100644 common/djangoapps/dark_lang/tests.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 501baee768..e0be8b21b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ Blades: Fix bug when image response in Firefox does not retain input. BLD-711. Blades: Give numerical response tolerance as a range. BLD-25. +Common: Add the ability to dark-launch site translations. These languages + will be unavailable to users except through the use of a specific query + parameter. + Blades: Allow user with BetaTester role correctly use LTI. BLD-641. Blades: Video player persist speed preferences between videos. BLD-237. diff --git a/cms/envs/aws.py b/cms/envs/aws.py index a781576455..afdd18288d 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -153,6 +153,12 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) #Timezone overrides TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) +# Translation overrides +LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) +RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES) +LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) +USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) + ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {})) for feature, value in ENV_FEATURES.items(): FEATURES[feature] = value diff --git a/cms/envs/common.py b/cms/envs/common.py index 4dc75f87e9..cf827a7320 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -167,6 +167,9 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'track.middleware.TrackMiddleware', + # Allows us to dark-launch particular languages + 'dark_lang.middleware.DarkLangMiddleware', + # Detects user-requested locale from 'accept-language' header in http request 'django.middleware.locale.LocaleMiddleware', @@ -244,12 +247,16 @@ STATICFILES_DIRS = [ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html -# We want i18n to be turned off in production, at least until we have full localizations. -# Thus we want the Django translation engine to be disabled. Otherwise even without -# localization files, if the user's browser is set to a language other than us-en, -# strings like "login" and "password" will be translated and the rest of the page will be -# in English, which is confusing. -USE_I18N = False +LANGUAGES = ( + ('en@pirate', 'Pirate English'), + ('eo', 'Esperanto'), +) + +# This is the list of language codes for languanges which are released to all users. +# See dark_lang/README.rst for more details. +RELEASED_LANGUAGES = () + +USE_I18N = True USE_L10N = True # Localization strings (e.g. django.po) are under this directory diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 16efed18c8..cd3e2067f4 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -9,11 +9,6 @@ from .common import * from logsettings import get_logger_config DEBUG = True -USE_I18N = True -# For displaying the dummy text, we need to provide a language mapping. -LANGUAGES = ( - ('eo', 'Esperanto'), -) TEMPLATE_DEBUG = DEBUG LOGGING = get_logger_config(ENV_ROOT / "log", logging_env="dev", diff --git a/common/djangoapps/dark_lang/README.rst b/common/djangoapps/dark_lang/README.rst new file mode 100644 index 0000000000..75d3224c20 --- /dev/null +++ b/common/djangoapps/dark_lang/README.rst @@ -0,0 +1,16 @@ +Language Translation Dark Launching +=================================== + +This app adds the ability to launch language translations that +are only accessible through the use of a specific query parameter +(and are not activated by browser settings). + +Installation +------------ + +Add the ``.DarkLangMiddleware`` to your list of ``MIDDLEWARE_CLASSES``. +It must come after the ``SessionMiddleware``, and before the ``LocaleMiddleware``. + +Add the ``RELEASED_LANGUAGES`` setting to your settings file. This +should be a list of all language codes which can be selected via a +user's browser settings. \ No newline at end of file diff --git a/common/djangoapps/dark_lang/__init__.py b/common/djangoapps/dark_lang/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py new file mode 100644 index 0000000000..9afddbc8ea --- /dev/null +++ b/common/djangoapps/dark_lang/middleware.py @@ -0,0 +1,87 @@ +""" +Middleware for dark-launching languages. These languages won't be used +when determining which translation to give a user based on their browser +header, but can be selected by setting the ``preview-lang`` query parameter +to the language code. + +Adding the query parameter ``clear-lang`` will reset the language stored +in the user's session. + +This middleware must be placed before the LocaleMiddleware, but after +the SessionMiddleware. +""" + +from django.conf import settings +from django.core.exceptions import MiddlewareNotUsed +from django.utils.translation.trans_real import parse_accept_lang_header + + +class DarkLangMiddleware(object): + """ + Middleware for dark-launching languages. + + This middleware will only be active if the RELEASED_LANGUAGES setting is set. + This setting should contain a list of language codes for languages which + are considered to be dark-launched, and those won't activate based on a + users browser settings. + """ + + def __init__(self): + self.released_langs = getattr(settings, 'RELEASED_LANGUAGES', None) + + if self.released_langs is None: + raise MiddlewareNotUsed() + + def process_request(self, request): + self._clean_accept_headers(request) + self._activate_preview_language(request) + + def _is_released(self, lang_code): + """ + ``True`` iff one of the values in ``self.released_langs`` is a prefix of ``lang_code``. + """ + return any(lang_code.startswith(released_lang) for released_lang in self.released_langs) + + def _format_accept_value(self, lang, priority=1.0): + """ + Formats lang and priority into a valid accept header fragment. + """ + return "{};q={}".format(lang, priority) + + def _clean_accept_headers(self, request): + """ + Remove any language that is not either in ``self.released_langs`` or + a territory of one of those languages. + """ + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None) + if accept is None or accept == '*': + return + + new_accept = ", ".join( + self._format_accept_value(lang, priority) + for lang, priority + in parse_accept_lang_header(accept) + if self._is_released(lang) + ) + + request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept + + def _activate_preview_language(self, request): + """ + If the request has the get parameter ``preview-lang``, + and that language appears doesn't appear in ``self.released_langs``, + then set the session ``django_language`` to that language. + """ + if 'clear-lang' in request.GET: + if 'django_language' in request.session: + del request.session['django_language'] + + preview_lang = request.GET.get('preview-lang', None) + + if not preview_lang: + return + + if preview_lang in self.released_langs: + return + + request.session['django_language'] = preview_lang diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py new file mode 100644 index 0000000000..e1de64cb3e --- /dev/null +++ b/common/djangoapps/dark_lang/tests.py @@ -0,0 +1,155 @@ +""" +Tests of DarkLangMiddleware +""" + +from django.core.exceptions import MiddlewareNotUsed +from django.http import HttpRequest, QueryDict + +from django.test import TestCase +from django.test.utils import override_settings +from mock import Mock + +from dark_lang.middleware import DarkLangMiddleware + + +UNSET = object() + + +def set_if_set(dict, key, value): + """ + Sets ``key`` in ``dict`` to ``value`` + unless ``value`` is ``UNSET`` + """ + if value is not UNSET: + dict[key] = value + + +@override_settings(RELEASED_LANGUAGES=('rel')) +class DarkLangMiddlewareTests(TestCase): + """ + Tests of DarkLangMiddleware + """ + def process_request(self, django_language=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET): + session = {} + set_if_set(session, 'django_language', django_language) + + META = {} + set_if_set(META, 'HTTP_ACCEPT_LANGUAGE', accept) + + GET = {} + set_if_set(GET, 'preview-lang', preview_lang) + set_if_set(GET, 'clear-lang', clear_lang) + + request = Mock( + spec=HttpRequest, + session=session, + META=META, + GET=GET + ) + self.assertIsNone(DarkLangMiddleware().process_request(request)) + return request + + @override_settings(RELEASED_LANGUAGES=None) + def test_inactive_middleware(self): + with self.assertRaises(MiddlewareNotUsed): + DarkLangMiddleware() + + def assertAcceptEquals(self, value, request): + """ + Assert that the HTML_ACCEPT_LANGUAGE header in request + is equal to value + """ + self.assertEquals( + value, + request.META.get('HTTP_ACCEPT_LANGUAGE', UNSET) + ) + + def test_empty_accept(self): + self.assertAcceptEquals(UNSET, self.process_request()) + + def test_wildcard_accept(self): + self.assertAcceptEquals('*', self.process_request(accept='*')) + + def test_released_accept(self): + self.assertAcceptEquals( + 'rel;q=1.0', + self.process_request(accept='rel;q=1.0') + ) + + def test_unreleased_accept(self): + self.assertAcceptEquals( + 'rel;q=1.0', + self.process_request(accept='rel;q=1.0, unrel;q=0.5') + ) + + @override_settings(RELEASED_LANGUAGES=('rel', 'unrel')) + def test_accept_multiple_released_langs(self): + + self.assertAcceptEquals( + 'rel;q=1.0, unrel;q=0.5', + self.process_request(accept='rel;q=1.0, unrel;q=0.5') + ) + + self.assertAcceptEquals( + 'rel;q=1.0, unrel;q=0.5', + self.process_request(accept='rel;q=1.0, notrel;q=0.3, unrel;q=0.5') + ) + + self.assertAcceptEquals( + 'rel;q=1.0, unrel;q=0.5', + self.process_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5') + ) + + def test_accept_released_territory(self): + self.assertAcceptEquals( + 'rel-ter;q=1.0, rel;q=0.5', + self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') + ) + + def assertSessionLangEquals(self, value, request): + """ + Assert that the 'django_language' set in request.session is equal to value + """ + self.assertEquals( + value, + request.session.get('django_language', UNSET) + ) + + def test_preview_lang_with_released_language(self): + self.assertSessionLangEquals( + UNSET, + self.process_request(preview_lang='rel') + ) + + self.assertSessionLangEquals( + 'notrel', + self.process_request(preview_lang='rel', django_language='notrel') + ) + + def test_preview_lang_with_dark_language(self): + self.assertSessionLangEquals( + 'unrel', + self.process_request(preview_lang='unrel') + ) + + self.assertSessionLangEquals( + 'unrel', + self.process_request(preview_lang='unrel', django_language='notrel') + ) + + def test_clear_lang(self): + self.assertSessionLangEquals( + UNSET, + self.process_request(clear_lang=True) + ) + + self.assertSessionLangEquals( + UNSET, + self.process_request(clear_lang=True, django_language='rel') + ) + + self.assertSessionLangEquals( + UNSET, + self.process_request(clear_lang=True, django_language='unrel') + ) + diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 0f1873e688..a536ec59be 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -203,6 +203,7 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) # Translation overrides LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) +RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) diff --git a/lms/envs/common.py b/lms/envs/common.py index 9d9ef25591..97a1df6367 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -492,14 +492,17 @@ FAVICON_PATH = 'images/favicon.ico' # Locale/Internationalization TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGES = () -# We want i18n to be turned off in production, at least until we have full localizations. -# Thus we want the Django translation engine to be disabled. Otherwise even without -# localization files, if the user's browser is set to a language other than us-en, -# strings like "login" and "password" will be translated and the rest of the page will be -# in English, which is confusing. -USE_I18N = False +LANGUAGES = ( + ('en@pirate', 'Pirate English'), + ('eo', 'Esperanto'), +) + +# This is the list of language codes for languanges which are released to all users. +# See dark_lang/README.rst for more details. +RELEASED_LANGUAGES = () + +USE_I18N = True USE_L10N = True # Localization strings (e.g. django.po) are under this directory @@ -639,6 +642,9 @@ MIDDLEWARE_CLASSES = ( 'course_wiki.course_nav.Middleware', + # Allows us to dark-launch particular languages + 'dark_lang.middleware.DarkLangMiddleware', + # Detects user-requested locale from 'accept-language' header in http request 'django.middleware.locale.LocaleMiddleware', diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 8f40887433..fc486726bf 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -16,11 +16,6 @@ from .common import * from logsettings import get_logger_config DEBUG = True -USE_I18N = True -# For displaying the dummy text, we need to provide a language mapping. -LANGUAGES = ( - ('eo', 'Esperanto'), -) TEMPLATE_DEBUG = True FEATURES['DISABLE_START_DATES'] = False From d379b35fa93f949264ca18879a2144174fe0b48f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 24 Jan 2014 16:08:05 -0500 Subject: [PATCH 2/3] Add config_model, a library for database backed configuration ConfigurationModels can be managed using the admin site. They are append-only, and track the user who is making the change, and the time that the change was made. The configuration is stored in the database, and cached for performance. [LMS-1220] --- CHANGELOG.rst | 4 + cms/envs/common.py | 3 + common/djangoapps/config_models/README.rst | 0 common/djangoapps/config_models/__init__.py | 62 ++++++++++++++ common/djangoapps/config_models/admin.py | 80 +++++++++++++++++++ common/djangoapps/config_models/models.py | 62 ++++++++++++++ .../djangoapps/config_models/templatetags.py | 29 +++++++ common/djangoapps/config_models/tests.py | 76 ++++++++++++++++++ lms/envs/common.py | 3 + rakelib/tests.rake | 9 ++- requirements/edx/base.txt | 15 ++-- 11 files changed, 334 insertions(+), 9 deletions(-) create mode 100644 common/djangoapps/config_models/README.rst create mode 100644 common/djangoapps/config_models/__init__.py create mode 100644 common/djangoapps/config_models/admin.py create mode 100644 common/djangoapps/config_models/models.py create mode 100644 common/djangoapps/config_models/templatetags.py create mode 100644 common/djangoapps/config_models/tests.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e0be8b21b6..fbdbb611a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ Blades: Fix bug when image response in Firefox does not retain input. BLD-711. Blades: Give numerical response tolerance as a range. BLD-25. +Common: Add a utility app for building databased-backed configuration + for specific application features. Includes admin site customization + for easier administration and tracking. + Common: Add the ability to dark-launch site translations. These languages will be unavailable to users except through the use of a specific query parameter. diff --git a/cms/envs/common.py b/cms/envs/common.py index cf827a7320..808bf925ca 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -413,6 +413,9 @@ INSTALLED_APPS = ( 'south', 'method_override', + # Database-backed configuration + 'config_models', + # Monitor the status of services 'service_status', diff --git a/common/djangoapps/config_models/README.rst b/common/djangoapps/config_models/README.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/config_models/__init__.py b/common/djangoapps/config_models/__init__.py new file mode 100644 index 0000000000..3f71f1c98e --- /dev/null +++ b/common/djangoapps/config_models/__init__.py @@ -0,0 +1,62 @@ +""" +Model-Based Configuration +========================= + +This app allows other apps to easily define a configuration model +that can be hooked into the admin site to allow configuration management +with auditing. + +Installation +------------ + +Add ``config_models`` to your ``INSTALLED_APPS`` list. + +Usage +----- + +Create a subclass of ``ConfigurationModel``, with fields for each +value that needs to be configured:: + + class MyConfiguration(ConfigurationModel): + frobble_timeout = IntField(default=10) + frazzle_target = TextField(defalut="debug") + +This is a normal django model, so it must be synced and migrated as usual. + +The default values for the fields in the ``ConfigurationModel`` will be +used if no configuration has yet been created. + +Register that class with the Admin site, using the ``ConfigurationAdminModel``:: + + from django.contrib import admin + + from config_models.admin import ConfigurationModelAdmin + + admin.site.register(MyConfiguration, ConfigurationModelAdmin) + +Use the configuration in your code:: + + def my_view(self, request): + config = MyConfiguration.current() + fire_the_missiles(config.frazzle_target, timeout=config.frobble_timeout) + +Use the admin site to add new configuration entries. The most recently created +entry is considered to be ``current``. + +Configuration +------------- + +The current ``ConfigurationModel`` will be cached in the ``configuration`` django cache, +or in the ``default`` cache if ``configuration`` doesn't exist. You can specify the cache +timeout in each ``ConfigurationModel`` by setting the ``cache_timeout`` property. + +You can change the name of the cache key used by the ``ConfigurationModel`` by overriding +the ``cache_key_name`` function. + +Extension +--------- + +``ConfigurationModels`` are just django models, so they can be extended with new fields +and migrated as usual. Newly added fields must have default values and should be nullable, +so that rollbacks to old versions of configuration work correctly. +""" diff --git a/common/djangoapps/config_models/admin.py b/common/djangoapps/config_models/admin.py new file mode 100644 index 0000000000..378900f1dc --- /dev/null +++ b/common/djangoapps/config_models/admin.py @@ -0,0 +1,80 @@ +""" +Admin site models for managing :class:`.ConfigurationModel` subclasses +""" + +from django.forms import models +from django.contrib import admin +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse + +# pylint: disable=protected-access + + +class ConfigurationModelAdmin(admin.ModelAdmin): + """ + :class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses + """ + date_hierarchy = 'change_date' + + def get_actions(self, request): + return { + 'revert': (ConfigurationModelAdmin.revert, 'revert', 'Revert to the selected configuration') + } + + def get_list_display(self, request): + return self.model._meta.get_all_field_names() + + # Don't allow deletion of configuration + def has_delete_permission(self, request, obj=None): + return False + + # Make all fields read-only when editing an object + def get_readonly_fields(self, request, obj=None): + if obj: # editing an existing object + return self.model._meta.get_all_field_names() + return self.readonly_fields + + def add_view(self, request, form_url='', extra_context=None): + # Prepopulate new configuration entries with the value of the current config + get = request.GET.copy() + get.update(models.model_to_dict(self.model.current())) + request.GET = get + return super(ConfigurationModelAdmin, self).add_view(request, form_url, extra_context) + + # Hide the save buttons in the change view + def change_view(self, request, object_id, form_url='', extra_context=None): + extra_context = extra_context or {} + extra_context['readonly'] = True + return super(ConfigurationModelAdmin, self).change_view( + request, + object_id, + form_url, + extra_context=extra_context + ) + + def save_model(self, request, obj, form, change): + obj.changed_by = request.user + super(ConfigurationModelAdmin, self).save_model(request, obj, form, change) + + def revert(self, request, queryset): + """ + Admin action to revert a configuration back to the selected value + """ + if queryset.count() != 1: + self.message_user(request, "Please select a single configuration to revert to.") + return + + target = queryset[0] + target.id = None + self.save_model(request, target, None, False) + self.message_user(request, "Reverted configuration.") + + return HttpResponseRedirect( + reverse( + 'admin:{}_{}_change'.format( + self.model._meta.app_label, + self.model._meta.module_name, + ), + args=(target.id,), + ) + ) diff --git a/common/djangoapps/config_models/models.py b/common/djangoapps/config_models/models.py new file mode 100644 index 0000000000..4a46e384f2 --- /dev/null +++ b/common/djangoapps/config_models/models.py @@ -0,0 +1,62 @@ +""" +Django Model baseclass for database-backed configuration. +""" +from django.db import models +from django.contrib.auth.models import User +from django.core.cache import get_cache, InvalidCacheBackendError + +try: + cache = get_cache('configuration') # pylint: disable=invalid-name +except InvalidCacheBackendError: + from django.core.cache import cache + + +class ConfigurationModel(models.Model): + """ + Abstract base class for model-based configuration + + Properties: + cache_timeout (int): The number of seconds that this configuration + should be cached + """ + + class Meta(object): # pylint: disable=missing-docstring + abstract = True + + # The number of seconds + cache_timeout = 600 + + change_date = models.DateTimeField(auto_now_add=True) + changed_by = models.ForeignKey(User, editable=False) + enabled = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + """ + Clear the cached value when saving a new configuration entry + """ + super(ConfigurationModel, self).save(*args, **kwargs) + cache.delete(self.cache_key_name()) + + @classmethod + def cache_key_name(cls): + """Return the name of the key to use to cache the current configuration""" + return 'configuration/{}/current'.format(cls.__name__) + + @classmethod + def current(cls): + """ + Return the active configuration entry, either from cache, + from the database, or by creating a new empty entry (which is not + persisted). + """ + cached = cache.get(cls.cache_key_name()) + if cached is not None: + return cached + + try: + current = cls.objects.order_by('-change_date')[0] + except IndexError: + current = cls() + + cache.set(cls.cache_key_name(), current, cls.cache_timeout) + return current diff --git a/common/djangoapps/config_models/templatetags.py b/common/djangoapps/config_models/templatetags.py new file mode 100644 index 0000000000..8641fd11ea --- /dev/null +++ b/common/djangoapps/config_models/templatetags.py @@ -0,0 +1,29 @@ +""" +Override the submit_row template tag to remove all save buttons from the +admin dashboard change view if the context has readonly marked in it. +""" + +from django.contrib.admin.templatetags.admin_modify import register +from django.contrib.admin.templatetags.admin_modify import submit_row as original_submit_row + + +@register.inclusion_tag('admin/submit_line.html', takes_context=True) +def submit_row(context): + """ + Overrides 'django.contrib.admin.templatetags.admin_modify.submit_row'. + + Manipulates the context going into that function by hiding all of the buttons + in the submit row if the key `readonly` is set in the context. + """ + ctx = original_submit_row(context) + + if context.get('readonly', False): + ctx.update({ + 'show_delete_link': False, + 'show_save_as_new': False, + 'show_save_and_add_another': False, + 'show_save_and_continue': False, + 'show_save': False, + }) + else: + return ctx diff --git a/common/djangoapps/config_models/tests.py b/common/djangoapps/config_models/tests.py new file mode 100644 index 0000000000..bb14ad18e8 --- /dev/null +++ b/common/djangoapps/config_models/tests.py @@ -0,0 +1,76 @@ +""" +Tests of ConfigurationModel +""" + +from django.contrib.auth.models import User +from django.db import models +from django.test import TestCase + +from freezegun import freeze_time + +from mock import patch +from config_models.models import ConfigurationModel + + +class ExampleConfig(ConfigurationModel): + """ + Test model for testing ``ConfigurationModels``. + """ + cache_timeout = 300 + + string_field = models.TextField() + int_field = models.IntegerField(default=10) + + +@patch('config_models.models.cache') +class ConfigurationModelTests(TestCase): + """ + Tests of ConfigurationModel + """ + def setUp(self): + self.user = User() + self.user.save() + + def test_cache_deleted_on_save(self, mock_cache): + ExampleConfig(changed_by=self.user).save() + mock_cache.delete.assert_called_with(ExampleConfig.cache_key_name()) + + def test_cache_key_name(self, _mock_cache): + self.assertEquals(ExampleConfig.cache_key_name(), 'configuration/ExampleConfig/current') + + def test_no_config_empty_cache(self, mock_cache): + mock_cache.get.return_value = None + + current = ExampleConfig.current() + self.assertEquals(current.int_field, 10) + self.assertEquals(current.string_field, '') + mock_cache.set.assert_called_with(ExampleConfig.cache_key_name(), current, 300) + + def test_no_config_full_cache(self, mock_cache): + current = ExampleConfig.current() + self.assertEquals(current, mock_cache.get.return_value) + + def test_config_ordering(self, mock_cache): + mock_cache.get.return_value = None + + with freeze_time('2012-01-01'): + first = ExampleConfig(changed_by=self.user) + first.string_field = 'first' + first.save() + + second = ExampleConfig(changed_by=self.user) + second.string_field = 'second' + second.save() + + self.assertEquals(ExampleConfig.current().string_field, 'second') + + def test_cache_set(self, mock_cache): + mock_cache.get.return_value = None + + first = ExampleConfig(changed_by=self.user) + first.string_field = 'first' + first.save() + + ExampleConfig.current() + + mock_cache.set.assert_called_with(ExampleConfig.cache_key_name(), first, 300) diff --git a/lms/envs/common.py b/lms/envs/common.py index 97a1df6367..5557ce9c9e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -983,6 +983,9 @@ INSTALLED_APPS = ( 'djcelery', 'south', + # Database-backed configuration + 'config_models', + # Monitor the status of services 'service_status', diff --git a/rakelib/tests.rake b/rakelib/tests.rake index 868fa5836f..e09c5d0762 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -22,10 +22,15 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true) # to the Djangoapps we want to test. Otherwise, it will # run tests on all installed packages. - default_test_id = "#{system}/djangoapps common/djangoapps" + # We need to use $DIR/*, rather than just $DIR so that + # django-nose will import them early in the test process, + # thereby making sure that we load any django models that are + # only defined in test files. + + default_test_id = "#{system}/djangoapps/* common/djangoapps/*" if system == :lms || system == :cms - default_test_id += " #{system}/lib" + default_test_id += " #{system}/lib/*" end if test_id.nil? diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index bce6479748..3df9a7034a 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -94,21 +94,22 @@ transifex-client==0.10 # Used for testing coverage==3.7 ddt==0.6.0 +django-crum==0.5 +django-debug-toolbar-mongo +django_debug_toolbar==0.9.4 +django_nose==1.1 factory_boy==2.2.1 +freezegun==0.1.11 mock==1.0.1 +nose-exclude +nose-ignore-docstring nosexcover==1.0.7 pep8==1.4.5 pylint==0.28 +python-subunit==0.0.16 rednose==0.3 selenium==2.34.0 splinter==0.5.4 -django_nose==1.1 -django_debug_toolbar==0.9.4 -django-debug-toolbar-mongo -nose-ignore-docstring -nose-exclude -django-crum==0.5 -python-subunit==0.0.16 testtools==0.9.34 git+https://github.com/mfogel/django-settings-context-processor.git From 271fbdb40a9f66a2673e230da5f070eff0d86923 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 24 Jan 2014 16:41:04 -0500 Subject: [PATCH 3/3] Switch dark_lang to database backed configuration --- cms/envs/aws.py | 1 - cms/envs/common.py | 10 +-- common/djangoapps/config_models/models.py | 2 +- common/djangoapps/dark_lang/README.rst | 16 ---- common/djangoapps/dark_lang/__init__.py | 19 +++++ common/djangoapps/dark_lang/admin.py | 10 +++ common/djangoapps/dark_lang/middleware.py | 27 ++++--- .../dark_lang/migrations/0001_initial.py | 74 ++++++++++++++++++ .../migrations/0002_enable_on_install.py | 68 ++++++++++++++++ .../dark_lang/migrations/__init__.py | 0 common/djangoapps/dark_lang/models.py | 26 +++++++ common/djangoapps/dark_lang/tests.py | 78 ++++++++++++++----- lms/envs/aws.py | 1 - lms/envs/common.py | 8 +- 14 files changed, 279 insertions(+), 61 deletions(-) delete mode 100644 common/djangoapps/dark_lang/README.rst create mode 100644 common/djangoapps/dark_lang/admin.py create mode 100644 common/djangoapps/dark_lang/migrations/0001_initial.py create mode 100644 common/djangoapps/dark_lang/migrations/0002_enable_on_install.py create mode 100644 common/djangoapps/dark_lang/migrations/__init__.py create mode 100644 common/djangoapps/dark_lang/models.py diff --git a/cms/envs/aws.py b/cms/envs/aws.py index afdd18288d..555fa39ec0 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -155,7 +155,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) # Translation overrides LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) -RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) diff --git a/cms/envs/common.py b/cms/envs/common.py index 808bf925ca..bd19b365b9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -248,14 +248,9 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGES = ( - ('en@pirate', 'Pirate English'), ('eo', 'Esperanto'), ) -# This is the list of language codes for languanges which are released to all users. -# See dark_lang/README.rst for more details. -RELEASED_LANGUAGES = () - USE_I18N = True USE_L10N = True @@ -448,7 +443,10 @@ INSTALLED_APPS = ( 'django.contrib.admin', # for managing course modes - 'course_modes' + 'course_modes', + + # Dark-launching languages + 'dark_lang', ) diff --git a/common/djangoapps/config_models/models.py b/common/djangoapps/config_models/models.py index 4a46e384f2..3c1d5d6061 100644 --- a/common/djangoapps/config_models/models.py +++ b/common/djangoapps/config_models/models.py @@ -27,7 +27,7 @@ class ConfigurationModel(models.Model): cache_timeout = 600 change_date = models.DateTimeField(auto_now_add=True) - changed_by = models.ForeignKey(User, editable=False) + changed_by = models.ForeignKey(User, editable=False, null=True, on_delete=models.PROTECT) enabled = models.BooleanField(default=False) def save(self, *args, **kwargs): diff --git a/common/djangoapps/dark_lang/README.rst b/common/djangoapps/dark_lang/README.rst deleted file mode 100644 index 75d3224c20..0000000000 --- a/common/djangoapps/dark_lang/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -Language Translation Dark Launching -=================================== - -This app adds the ability to launch language translations that -are only accessible through the use of a specific query parameter -(and are not activated by browser settings). - -Installation ------------- - -Add the ``.DarkLangMiddleware`` to your list of ``MIDDLEWARE_CLASSES``. -It must come after the ``SessionMiddleware``, and before the ``LocaleMiddleware``. - -Add the ``RELEASED_LANGUAGES`` setting to your settings file. This -should be a list of all language codes which can be selected via a -user's browser settings. \ No newline at end of file diff --git a/common/djangoapps/dark_lang/__init__.py b/common/djangoapps/dark_lang/__init__.py index e69de29bb2..d56fa38068 100644 --- a/common/djangoapps/dark_lang/__init__.py +++ b/common/djangoapps/dark_lang/__init__.py @@ -0,0 +1,19 @@ +""" +Language Translation Dark Launching +=================================== + +This app adds the ability to launch language translations that +are only accessible through the use of a specific query parameter +(and are not activated by browser settings). + +Installation +------------ + +Add the ``DarkLangMiddleware`` to your list of ``MIDDLEWARE_CLASSES``. +It must come after the ``SessionMiddleware``, and before the ``LocaleMiddleware``. + +Run migrations to install the configuration table. + +Use the admin site to add a new ``DarkLangConfig`` that is enabled, and lists the +languages that should be released. +""" diff --git a/common/djangoapps/dark_lang/admin.py b/common/djangoapps/dark_lang/admin.py new file mode 100644 index 0000000000..cc80e49b25 --- /dev/null +++ b/common/djangoapps/dark_lang/admin.py @@ -0,0 +1,10 @@ +""" +Admin site bindings for dark_lang +""" + +from django.contrib import admin + +from config_models.admin import ConfigurationModelAdmin +from dark_lang.models import DarkLangConfig + +admin.site.register(DarkLangConfig, ConfigurationModelAdmin) diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index 9afddbc8ea..88783328be 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -11,28 +11,33 @@ This middleware must be placed before the LocaleMiddleware, but after the SessionMiddleware. """ -from django.conf import settings -from django.core.exceptions import MiddlewareNotUsed from django.utils.translation.trans_real import parse_accept_lang_header +from dark_lang.models import DarkLangConfig + class DarkLangMiddleware(object): """ Middleware for dark-launching languages. - This middleware will only be active if the RELEASED_LANGUAGES setting is set. - This setting should contain a list of language codes for languages which - are considered to be dark-launched, and those won't activate based on a - users browser settings. + This is configured by creating ``DarkLangConfig`` rows in the database, + using the django admin site. """ - def __init__(self): - self.released_langs = getattr(settings, 'RELEASED_LANGUAGES', None) - - if self.released_langs is None: - raise MiddlewareNotUsed() + @property + def released_langs(self): + """ + Current list of released languages + """ + return DarkLangConfig.current().released_languages_list def process_request(self, request): + """ + Prevent user from requesting un-released languages except by using the preview-lang query string. + """ + if not DarkLangConfig.current().enabled: + return + self._clean_accept_headers(request) self._activate_preview_language(request) diff --git a/common/djangoapps/dark_lang/migrations/0001_initial.py b/common/djangoapps/dark_lang/migrations/0001_initial.py new file mode 100644 index 0000000000..cc715fe8e5 --- /dev/null +++ b/common/djangoapps/dark_lang/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'DarkLangConfig' + db.create_table('dark_lang_darklangconfig', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('released_languages', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('dark_lang', ['DarkLangConfig']) + + + def backwards(self, orm): + # Deleting model 'DarkLangConfig' + db.delete_table('dark_lang_darklangconfig') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'dark_lang.darklangconfig': { + 'Meta': {'object_name': 'DarkLangConfig'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'released_languages': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + } + } + + complete_apps = ['dark_lang'] \ No newline at end of file diff --git a/common/djangoapps/dark_lang/migrations/0002_enable_on_install.py b/common/djangoapps/dark_lang/migrations/0002_enable_on_install.py new file mode 100644 index 0000000000..c794a156ce --- /dev/null +++ b/common/djangoapps/dark_lang/migrations/0002_enable_on_install.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + """ + Enable DarkLang by default when it is installed, to prevent accidental + release of testing languages. + """ + orm.DarkLangConfig(enabled=True).save() + + def backwards(self, orm): + "Write your backwards methods here." + raise RuntimeError("Cannot reverse this migration.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'dark_lang.darklangconfig': { + 'Meta': {'object_name': 'DarkLangConfig'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'released_languages': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + } + } + + complete_apps = ['dark_lang'] + symmetrical = True diff --git a/common/djangoapps/dark_lang/migrations/__init__.py b/common/djangoapps/dark_lang/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/dark_lang/models.py b/common/djangoapps/dark_lang/models.py new file mode 100644 index 0000000000..9912287b4e --- /dev/null +++ b/common/djangoapps/dark_lang/models.py @@ -0,0 +1,26 @@ +""" +Models for the dark-launching languages +""" +from django.db import models + +from config_models.models import ConfigurationModel + + +class DarkLangConfig(ConfigurationModel): + """ + Configuration for the dark_lang django app + """ + released_languages = models.TextField( + blank=True, + help_text="A comma-separated list of language codes to release to the public." + ) + + @property + def released_languages_list(self): + """ + ``released_languages`` as a list of language codes. + """ + if not self.released_languages.strip(): # pylint: disable=no-member + return [] + + return [lang.strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py index e1de64cb3e..7667102b90 100644 --- a/common/djangoapps/dark_lang/tests.py +++ b/common/djangoapps/dark_lang/tests.py @@ -2,58 +2,70 @@ Tests of DarkLangMiddleware """ -from django.core.exceptions import MiddlewareNotUsed -from django.http import HttpRequest, QueryDict +from django.contrib.auth.models import User +from django.http import HttpRequest from django.test import TestCase -from django.test.utils import override_settings from mock import Mock from dark_lang.middleware import DarkLangMiddleware +from dark_lang.models import DarkLangConfig UNSET = object() -def set_if_set(dict, key, value): +def set_if_set(dct, key, value): """ - Sets ``key`` in ``dict`` to ``value`` + Sets ``key`` in ``dct`` to ``value`` unless ``value`` is ``UNSET`` """ if value is not UNSET: - dict[key] = value + dct[key] = value -@override_settings(RELEASED_LANGUAGES=('rel')) class DarkLangMiddlewareTests(TestCase): """ Tests of DarkLangMiddleware """ + def setUp(self): + self.user = User() + self.user.save() + DarkLangConfig( + released_languages='rel', + changed_by=self.user, + enabled=True + ).save() + def process_request(self, django_language=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET): + """ + Build a request and then process it using the ``DarkLangMiddleware``. + + Args: + django_language (str): The language code to set in request.session['django_language'] + accept (str): The accept header to set in request.META['HTTP_ACCEPT_LANGUAGE'] + preview_lang (str): The value to set in request.GET['preview_lang'] + clear_lang (str): The value to set in request.GET['clear_lang'] + """ session = {} set_if_set(session, 'django_language', django_language) - META = {} - set_if_set(META, 'HTTP_ACCEPT_LANGUAGE', accept) + meta = {} + set_if_set(meta, 'HTTP_ACCEPT_LANGUAGE', accept) - GET = {} - set_if_set(GET, 'preview-lang', preview_lang) - set_if_set(GET, 'clear-lang', clear_lang) + get = {} + set_if_set(get, 'preview-lang', preview_lang) + set_if_set(get, 'clear-lang', clear_lang) request = Mock( spec=HttpRequest, session=session, - META=META, - GET=GET + META=meta, + GET=get ) self.assertIsNone(DarkLangMiddleware().process_request(request)) return request - @override_settings(RELEASED_LANGUAGES=None) - def test_inactive_middleware(self): - with self.assertRaises(MiddlewareNotUsed): - DarkLangMiddleware() - def assertAcceptEquals(self, value, request): """ Assert that the HTML_ACCEPT_LANGUAGE header in request @@ -82,8 +94,12 @@ class DarkLangMiddlewareTests(TestCase): self.process_request(accept='rel;q=1.0, unrel;q=0.5') ) - @override_settings(RELEASED_LANGUAGES=('rel', 'unrel')) def test_accept_multiple_released_langs(self): + DarkLangConfig( + released_languages=('rel, unrel'), + changed_by=self.user, + enabled=True + ).save() self.assertAcceptEquals( 'rel;q=1.0, unrel;q=0.5', @@ -153,3 +169,25 @@ class DarkLangMiddlewareTests(TestCase): self.process_request(clear_lang=True, django_language='unrel') ) + def test_disabled(self): + DarkLangConfig(enabled=False, changed_by=self.user).save() + + self.assertAcceptEquals( + 'notrel;q=0.3, rel;q=1.0, unrel;q=0.5', + self.process_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5') + ) + + self.assertSessionLangEquals( + 'rel', + self.process_request(clear_lang=True, django_language='rel') + ) + + self.assertSessionLangEquals( + 'unrel', + self.process_request(clear_lang=True, django_language='unrel') + ) + + self.assertSessionLangEquals( + 'rel', + self.process_request(preview_lang='unrel', django_language='rel') + ) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index a536ec59be..0f1873e688 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -203,7 +203,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) # Translation overrides LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) -RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) diff --git a/lms/envs/common.py b/lms/envs/common.py index 5557ce9c9e..f453ab0369 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -494,14 +494,9 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGES = ( - ('en@pirate', 'Pirate English'), ('eo', 'Esperanto'), ) -# This is the list of language codes for languanges which are released to all users. -# See dark_lang/README.rst for more details. -RELEASED_LANGUAGES = () - USE_I18N = True USE_L10N = True @@ -1064,6 +1059,9 @@ INSTALLED_APPS = ( # Student Identity Verification 'verify_student', + + # Dark-launching languages + 'dark_lang', ) ######################### MARKETING SITE ###############################