Commit 271fbdb4 by Calen Pennington

Switch dark_lang to database backed configuration

parent d379b35f
...@@ -155,7 +155,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) ...@@ -155,7 +155,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
# Translation overrides # Translation overrides
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES)
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
......
...@@ -248,14 +248,9 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_ ...@@ -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 LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGES = ( LANGUAGES = (
('en@pirate', 'Pirate English'),
('eo', 'Esperanto'), ('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_I18N = True
USE_L10N = True USE_L10N = True
...@@ -448,7 +443,10 @@ INSTALLED_APPS = ( ...@@ -448,7 +443,10 @@ INSTALLED_APPS = (
'django.contrib.admin', 'django.contrib.admin',
# for managing course modes # for managing course modes
'course_modes' 'course_modes',
# Dark-launching languages
'dark_lang',
) )
......
...@@ -27,7 +27,7 @@ class ConfigurationModel(models.Model): ...@@ -27,7 +27,7 @@ class ConfigurationModel(models.Model):
cache_timeout = 600 cache_timeout = 600
change_date = models.DateTimeField(auto_now_add=True) 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) enabled = models.BooleanField(default=False)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
......
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
"""
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)
...@@ -11,28 +11,33 @@ This middleware must be placed before the LocaleMiddleware, but after ...@@ -11,28 +11,33 @@ This middleware must be placed before the LocaleMiddleware, but after
the SessionMiddleware. 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 django.utils.translation.trans_real import parse_accept_lang_header
from dark_lang.models import DarkLangConfig
class DarkLangMiddleware(object): class DarkLangMiddleware(object):
""" """
Middleware for dark-launching languages. Middleware for dark-launching languages.
This middleware will only be active if the RELEASED_LANGUAGES setting is set. This is configured by creating ``DarkLangConfig`` rows in the database,
This setting should contain a list of language codes for languages which using the django admin site.
are considered to be dark-launched, and those won't activate based on a
users browser settings.
""" """
def __init__(self): @property
self.released_langs = getattr(settings, 'RELEASED_LANGUAGES', None) def released_langs(self):
"""
if self.released_langs is None: Current list of released languages
raise MiddlewareNotUsed() """
return DarkLangConfig.current().released_languages_list
def process_request(self, request): 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._clean_accept_headers(request)
self._activate_preview_language(request) self._activate_preview_language(request)
......
# -*- 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
...@@ -2,58 +2,70 @@ ...@@ -2,58 +2,70 @@
Tests of DarkLangMiddleware Tests of DarkLangMiddleware
""" """
from django.core.exceptions import MiddlewareNotUsed from django.contrib.auth.models import User
from django.http import HttpRequest, QueryDict from django.http import HttpRequest
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from mock import Mock from mock import Mock
from dark_lang.middleware import DarkLangMiddleware from dark_lang.middleware import DarkLangMiddleware
from dark_lang.models import DarkLangConfig
UNSET = object() 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`` unless ``value`` is ``UNSET``
""" """
if value is not UNSET: if value is not UNSET:
dict[key] = value dct[key] = value
@override_settings(RELEASED_LANGUAGES=('rel'))
class DarkLangMiddlewareTests(TestCase): class DarkLangMiddlewareTests(TestCase):
""" """
Tests of DarkLangMiddleware 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): 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 = {} session = {}
set_if_set(session, 'django_language', django_language) set_if_set(session, 'django_language', django_language)
META = {} meta = {}
set_if_set(META, 'HTTP_ACCEPT_LANGUAGE', accept) set_if_set(meta, 'HTTP_ACCEPT_LANGUAGE', accept)
GET = {} get = {}
set_if_set(GET, 'preview-lang', preview_lang) set_if_set(get, 'preview-lang', preview_lang)
set_if_set(GET, 'clear-lang', clear_lang) set_if_set(get, 'clear-lang', clear_lang)
request = Mock( request = Mock(
spec=HttpRequest, spec=HttpRequest,
session=session, session=session,
META=META, META=meta,
GET=GET GET=get
) )
self.assertIsNone(DarkLangMiddleware().process_request(request)) self.assertIsNone(DarkLangMiddleware().process_request(request))
return request return request
@override_settings(RELEASED_LANGUAGES=None)
def test_inactive_middleware(self):
with self.assertRaises(MiddlewareNotUsed):
DarkLangMiddleware()
def assertAcceptEquals(self, value, request): def assertAcceptEquals(self, value, request):
""" """
Assert that the HTML_ACCEPT_LANGUAGE header in request Assert that the HTML_ACCEPT_LANGUAGE header in request
...@@ -82,8 +94,12 @@ class DarkLangMiddlewareTests(TestCase): ...@@ -82,8 +94,12 @@ class DarkLangMiddlewareTests(TestCase):
self.process_request(accept='rel;q=1.0, unrel;q=0.5') 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): def test_accept_multiple_released_langs(self):
DarkLangConfig(
released_languages=('rel, unrel'),
changed_by=self.user,
enabled=True
).save()
self.assertAcceptEquals( self.assertAcceptEquals(
'rel;q=1.0, unrel;q=0.5', 'rel;q=1.0, unrel;q=0.5',
...@@ -153,3 +169,25 @@ class DarkLangMiddlewareTests(TestCase): ...@@ -153,3 +169,25 @@ class DarkLangMiddlewareTests(TestCase):
self.process_request(clear_lang=True, django_language='unrel') 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')
)
...@@ -203,7 +203,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) ...@@ -203,7 +203,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
# Translation overrides # Translation overrides
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES)
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
......
...@@ -494,14 +494,9 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_ ...@@ -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 LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGES = ( LANGUAGES = (
('en@pirate', 'Pirate English'),
('eo', 'Esperanto'), ('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_I18N = True
USE_L10N = True USE_L10N = True
...@@ -1064,6 +1059,9 @@ INSTALLED_APPS = ( ...@@ -1064,6 +1059,9 @@ INSTALLED_APPS = (
# Student Identity Verification # Student Identity Verification
'verify_student', 'verify_student',
# Dark-launching languages
'dark_lang',
) )
######################### MARKETING SITE ############################### ######################### MARKETING SITE ###############################
......
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