Commit ce19f288 by Calen Pennington

Merge pull request #2294 from cpennington/language-dark-launch

Add the ability to dark-launch languages
parents 9c6a7a3e 271fbdb4
......@@ -12,6 +12,14 @@ 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.
Blades: Allow user with BetaTester role correctly use LTI. BLD-641.
Blades: Video player persist speed preferences between videos. BLD-237.
......
......@@ -153,6 +153,11 @@ 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)
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
......
......@@ -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,11 @@ 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 = (
('eo', 'Esperanto'),
)
USE_I18N = True
USE_L10N = True
# Localization strings (e.g. django.po) are under this directory
......@@ -406,6 +408,9 @@ INSTALLED_APPS = (
'south',
'method_override',
# Database-backed configuration
'config_models',
# Monitor the status of services
'service_status',
......@@ -438,7 +443,10 @@ INSTALLED_APPS = (
'django.contrib.admin',
# for managing course modes
'course_modes'
'course_modes',
# Dark-launching languages
'dark_lang',
)
......
......@@ -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",
......
"""
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.
"""
"""
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,),
)
)
"""
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, null=True, on_delete=models.PROTECT)
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
"""
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
"""
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)
"""
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.
"""
"""
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)
"""
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.utils.translation.trans_real import parse_accept_lang_header
from dark_lang.models import DarkLangConfig
class DarkLangMiddleware(object):
"""
Middleware for dark-launching languages.
This is configured by creating ``DarkLangConfig`` rows in the database,
using the django admin site.
"""
@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)
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
# -*- 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
# -*- 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
"""
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
"""
Tests of DarkLangMiddleware
"""
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.test import TestCase
from mock import Mock
from dark_lang.middleware import DarkLangMiddleware
from dark_lang.models import DarkLangConfig
UNSET = object()
def set_if_set(dct, key, value):
"""
Sets ``key`` in ``dct`` to ``value``
unless ``value`` is ``UNSET``
"""
if value is not UNSET:
dct[key] = value
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)
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
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')
)
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',
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')
)
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')
)
......@@ -492,14 +492,12 @@ 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 = (
('eo', 'Esperanto'),
)
USE_I18N = True
USE_L10N = True
# Localization strings (e.g. django.po) are under this directory
......@@ -639,6 +637,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',
......@@ -977,6 +978,9 @@ INSTALLED_APPS = (
'djcelery',
'south',
# Database-backed configuration
'config_models',
# Monitor the status of services
'service_status',
......@@ -1055,6 +1059,9 @@ INSTALLED_APPS = (
# Student Identity Verification
'verify_student',
# Dark-launching languages
'dark_lang',
)
######################### MARKETING SITE ###############################
......
......@@ -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
......
......@@ -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?
......
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment