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)
# 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)
......
......@@ -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',
)
......
......@@ -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):
......
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
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)
......
# -*- 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 @@
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')
)
......@@ -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)
......
......@@ -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 ###############################
......
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