Merge pull request #2294 from cpennington/language-dark-launch
Add the ability to dark-launch languages
This commit is contained in:
@@ -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",
|
||||
|
||||
0
common/djangoapps/config_models/README.rst
Normal file
0
common/djangoapps/config_models/README.rst
Normal file
62
common/djangoapps/config_models/__init__.py
Normal file
62
common/djangoapps/config_models/__init__.py
Normal file
@@ -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.
|
||||
"""
|
||||
80
common/djangoapps/config_models/admin.py
Normal file
80
common/djangoapps/config_models/admin.py
Normal file
@@ -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,),
|
||||
)
|
||||
)
|
||||
62
common/djangoapps/config_models/models.py
Normal file
62
common/djangoapps/config_models/models.py
Normal file
@@ -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, 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
|
||||
29
common/djangoapps/config_models/templatetags.py
Normal file
29
common/djangoapps/config_models/templatetags.py
Normal file
@@ -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
|
||||
76
common/djangoapps/config_models/tests.py
Normal file
76
common/djangoapps/config_models/tests.py
Normal file
@@ -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)
|
||||
19
common/djangoapps/dark_lang/__init__.py
Normal file
19
common/djangoapps/dark_lang/__init__.py
Normal file
@@ -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.
|
||||
"""
|
||||
10
common/djangoapps/dark_lang/admin.py
Normal file
10
common/djangoapps/dark_lang/admin.py
Normal file
@@ -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)
|
||||
92
common/djangoapps/dark_lang/middleware.py
Normal file
92
common/djangoapps/dark_lang/middleware.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
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
|
||||
74
common/djangoapps/dark_lang/migrations/0001_initial.py
Normal file
74
common/djangoapps/dark_lang/migrations/0001_initial.py
Normal file
@@ -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']
|
||||
@@ -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
|
||||
0
common/djangoapps/dark_lang/migrations/__init__.py
Normal file
0
common/djangoapps/dark_lang/migrations/__init__.py
Normal file
26
common/djangoapps/dark_lang/models.py
Normal file
26
common/djangoapps/dark_lang/models.py
Normal file
@@ -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
|
||||
193
common/djangoapps/dark_lang/tests.py
Normal file
193
common/djangoapps/dark_lang/tests.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user