Switch dark_lang to database backed configuration
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
Language Translation Dark Launching
|
||||
===================================
|
||||
|
||||
This app adds the ability to launch language translations that
|
||||
are only accessible through the use of a specific query parameter
|
||||
(and are not activated by browser settings).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Add the ``.DarkLangMiddleware`` to your list of ``MIDDLEWARE_CLASSES``.
|
||||
It must come after the ``SessionMiddleware``, and before the ``LocaleMiddleware``.
|
||||
|
||||
Add the ``RELEASED_LANGUAGES`` setting to your settings file. This
|
||||
should be a list of all language codes which can be selected via a
|
||||
user's browser settings.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
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
|
||||
@@ -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 ###############################
|
||||
|
||||
Reference in New Issue
Block a user