Skip to content

Commit

Permalink
[Issue mozmeao#997] Migrate TargetedLocales to Locale
Browse files Browse the repository at this point in the history
  • Loading branch information
glogiotatidis committed Jun 20, 2019
1 parent df49319 commit 82777da
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 107 deletions.
3 changes: 2 additions & 1 deletion snippets/base/admin/__init__.py
Expand Up @@ -4,7 +4,7 @@

from .adminmodels import (AddonAdmin, ASRSnippetAdmin, CampaignAdmin, CategoryAdmin,
ClientMatchRuleAdmin, LogEntryAdmin, IconAdmin,
SnippetTemplateAdmin, TargetAdmin)
SnippetTemplateAdmin, TargetAdmin, LocaleAdmin)
from .legacy import (SnippetAdmin, SearchProviderAdmin, TargetedCountryAdmin,
TargetedLocaleAdmin)

Expand All @@ -25,3 +25,4 @@
admin.site.register(models.SnippetTemplate, SnippetTemplateAdmin)
admin.site.register(models.Target, TargetAdmin)
admin.site.register(models.Icon, IconAdmin)
admin.site.register(models.Locale, LocaleAdmin)
27 changes: 14 additions & 13 deletions snippets/base/admin/adminmodels.py
Expand Up @@ -465,13 +465,13 @@ class ASRSnippetAdmin(admin.ModelAdmin):
'id',
'name',
'status',
'locale_list',
'locale',
'modified',
)
list_filter = (
filters.ModifiedFilter,
filters.TemplateFilter,
('locales', RelatedOnlyDropdownFilter),
('locale', RelatedDropdownFilter),
('targets', RelatedOnlyDropdownFilter),
'status',
filters.ChannelFilter,
Expand Down Expand Up @@ -503,7 +503,6 @@ class ASRSnippetAdmin(admin.ModelAdmin):
)
filter_horizontal = (
'targets',
'locales',
)
save_on_top = True
save_as = True
Expand Down Expand Up @@ -541,7 +540,10 @@ class ASRSnippetAdmin(admin.ModelAdmin):
<br/>
''' # noqa
),
'fields': ('template_chooser',),
'fields': (
'locale',
'template_chooser',
),
'classes': ('template-fieldset',)
}),
('Publishing Options', {
Expand All @@ -550,7 +552,6 @@ class ASRSnippetAdmin(admin.ModelAdmin):
'category',
'targets',
('publish_start', 'publish_end'),
'locales',
'weight',)
}),
('Other Info', {
Expand Down Expand Up @@ -686,14 +687,6 @@ def has_global_publish_permission(self, request):
]
])

def locale_list(self, obj):
num_locales = obj.locales.count()
locales = obj.locales.all()[:3]
active_locales = ', '.join([str(locale) for locale in locales])
if num_locales > 3:
active_locales += ' and {0} more.'.format(num_locales - 3)
return active_locales


class CampaignAdmin(RelatedSnippetsMixin, admin.ModelAdmin):
readonly_fields = [
Expand Down Expand Up @@ -897,3 +890,11 @@ def save_model(self, request, obj, form, change):
obj.creator = request.user
statsd.incr('save.target')
super().save_model(request, obj, form, change)


class LocaleAdmin(admin.ModelAdmin):
list_display = ('name', 'code')
search_fields = (
'name',
'code',
)
19 changes: 14 additions & 5 deletions snippets/base/feed.py
@@ -1,28 +1,37 @@
import operator
from datetime import timedelta
from distutils.util import strtobool
from textwrap import dedent
from urllib.parse import urlparse

from django.conf import settings
from django.db.models import Q

import django_filters
from django_ical.views import ICalFeed

from snippets.base import models


class CharInFilter(django_filters.BaseInFilter, django_filters.CharFilter):
pass


class ASRSnippetFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
locale = CharInFilter(field_name='locales__code', lookup_expr='in')
locale = django_filters.CharFilter(method='filter_locale')
only_scheduled = django_filters.ChoiceFilter(
method='filter_scheduled', choices=(('true', 'Yes'),
('false', 'No'),
('all', 'All')))

def filter_locale(self, queryset, name, value):
if not value:
return queryset

locales = value.split(',')
return queryset.filter(
operator.or_(
*[Q(locale__code=',{},'.format(locale)) for locale in locales]
)
)

def filter_scheduled(self, queryset, name, value):
if value == 'all':
return queryset
Expand Down
28 changes: 11 additions & 17 deletions snippets/base/managers.py
@@ -1,6 +1,6 @@
from datetime import datetime

from django.db.models import Manager
from django.db.models import Manager, Q
from django.db.models.query import QuerySet

from product_details import product_details
Expand Down Expand Up @@ -109,29 +109,23 @@ def filter_by_available(self):
def match_client(self, client):
from snippets.base.models import CHANNELS, ClientMatchRule, Target

snippet_filters = {}
target_filters = {}

# Retrieve the first channel that starts with the client's channel.
# Allows things like "release-cck-mozilla14" to match "release".
if client.channel == 'default':
client_channel = 'nightly'
else:
client_channel = first(CHANNELS, client.channel.startswith)
if client_channel:
target_filters.update(**{'on_{0}'.format(client_channel): True})
targets = Target.objects.filter(**target_filters).distinct()
client_channel = first(CHANNELS, client.channel.startswith) or 'release'

# Only filter by locale if they pass a valid locale.
locales = list(filter(client.locale.lower().startswith, LANGUAGE_VALUES))
if locales:
snippet_filters.update(locales__code__in=locales)
else:
# If the locale is invalid, only match snippets with no
# locales specified.
snippet_filters.update(locales__isnull=True)
targets = Target.objects.filter(**{'on_{0}'.format(client_channel): True}).distinct()

# Include both Snippets targeted at the specific full locale (e.g.
# en-us) but also snippets targeted to all territories (en)
full_locale = ',{},'.format(client.locale.lower())
splitted_locale = ',{},'.format(client.locale.lower().split('-', 1)[0])
snippets = self.filter(Q(locale__code__contains=splitted_locale) |
Q(locale__code__contains=full_locale))

snippets = self.filter(**snippet_filters).filter(targets__in=targets)
snippets = snippets.filter(targets__in=targets)

# Filter based on ClientMatchRules
passed_rules, failed_rules = (ClientMatchRule.objects
Expand Down
33 changes: 33 additions & 0 deletions snippets/base/migrations/0094_auto_20190619_1039.py
@@ -0,0 +1,33 @@
# Generated by Django 2.2.1 on 2019-06-19 10:39

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import snippets.base.validators


class Migration(migrations.Migration):

dependencies = [
('base', '0093_auto_20190618_1325'),
]

operations = [
migrations.CreateModel(
name='Locale',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('code', models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator(regex='^,?([A-Za-z-]+,?)+$')])),
('translations', models.TextField(blank=True, default='{}', help_text='JSON dictionary with Template fields as keys and localized strings as values.', validators=[snippets.base.validators.validate_json_data])),
],
options={
'ordering': ('name', 'code'),
},
),
migrations.AddField(
model_name='asrsnippet',
name='locale',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='base.Locale'),
),
]
28 changes: 28 additions & 0 deletions snippets/base/models.py
Expand Up @@ -1373,6 +1373,33 @@ def get_rich_text_fields(self):
return ['text']


class Locale(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(
max_length=255, unique=True,
validators=[django_validators.RegexValidator(regex=r'^,?([A-Za-z-]+,?)+$')],
)
translations = models.TextField(
blank=True, validators=[validators.validate_json_data], default='{}',
help_text='JSON dictionary with Template fields as keys and localized strings as values.'
)

def save(self, *args, **kwargs):
# Make sure that code always stats and ends with `,` and it's always lowercase.
self.code = self.code.lower()
if self.code[0] != ',':
self.code = ',' + self.code
if self.code[-1] != ',':
self.code = self.code + ','
super().save(*args, **kwargs)

class Meta:
ordering = ('name', 'code')

def __str__(self):
return self.name


class ASRSnippet(models.Model):
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -1401,6 +1428,7 @@ class ASRSnippet(models.Model):
'See the current time in <a target="_blank" href="http://time.is/UTC">UTC</a>'))

locales = models.ManyToManyField('TargetedLocale', blank=True, verbose_name='Targeted Locales')
locale = models.ForeignKey('Locale', blank=False, null=True, on_delete=models.PROTECT)
targets = models.ManyToManyField(Target, default=None, blank=True, related_name='snippets')

weight = models.IntegerField(
Expand Down
26 changes: 19 additions & 7 deletions snippets/base/tests/__init__.py
@@ -1,3 +1,6 @@
import random
import string

from django.test import TransactionTestCase

import factory
Expand Down Expand Up @@ -188,16 +191,17 @@ def targets(self, create, extracted, **kwargs):
self.targets.add(target)

@factory.post_generation
def locales(self, create, extracted, **kwargs):
def locale(self, create, extracted, **kwargs):
if not create:
return

if extracted is None:
extracted = ['en-us']

locales = [models.TargetedLocale.objects.get_or_create(code=code, name=code)[0]
for code in extracted]
self.locales.add(*locales)
code = extracted or 'en-us'
if code[0] != ',':
code = ',' + code
if code[-1] != ',':
code = code + ','
locale = models.Locale.objects.get_or_create(code=code, name=code)[0]
self.locale = locale


class AddonFactory(factory.django.DjangoModelFactory):
Expand All @@ -207,3 +211,11 @@ class AddonFactory(factory.django.DjangoModelFactory):

class Meta:
model = models.Addon


class LocaleFactory(factory.django.DjangoModelFactory):
name = factory.Sequence(lambda n: 'Locale {}'.format(n))
code = factory.LazyAttribute(lambda o: ''.join(random.choices(string.ascii_lowercase, k=4)))

class Meta:
model = models.Locale
6 changes: 3 additions & 3 deletions snippets/base/tests/test_feed.py
Expand Up @@ -48,9 +48,9 @@ def test_name(self):
self.assertEqual(set([snippet1]), set(filtr.qs))

def test_locale(self):
snippet1 = ASRSnippetFactory.create(locales=['xx', 'de'])
snippet2 = ASRSnippetFactory.create(locales=['fr'])
ASRSnippetFactory.create(locales=['de'])
snippet1 = ASRSnippetFactory.create(locale='xx')
snippet2 = ASRSnippetFactory.create(locale='fr')
ASRSnippetFactory.create(locale='de')
filtr = ASRSnippetFilter(QueryDict(query_string='locale=xx,fr'),
queryset=models.ASRSnippet.objects.all())
self.assertEqual(set([snippet1, snippet2]), set(filtr.qs))
Expand Down
6 changes: 4 additions & 2 deletions snippets/base/tests/test_forms.py
Expand Up @@ -9,8 +9,8 @@
SnippetAdminForm, TargetAdminForm,
TemplateDataWidget, TemplateSelect)
from snippets.base.models import STATUS_CHOICES
from snippets.base.tests import (ASRSnippetFactory, SnippetFactory,
SnippetTemplateFactory,
from snippets.base.tests import (ASRSnippetFactory, LocaleFactory,
SnippetFactory, SnippetTemplateFactory,
SnippetTemplateVariableFactory, TestCase,
TargetFactory)

Expand Down Expand Up @@ -265,6 +265,7 @@ def test_publish_permission_check_asr(self):
user = User.objects.create_user(username='admin',
email='foo@example.com',
password='admin')
locale = LocaleFactory()

perm_beta = Permission.objects.get(
codename='publish_on_beta',
Expand Down Expand Up @@ -292,6 +293,7 @@ def test_publish_permission_check_asr(self):
data = {
'name': 'Test',
'weight': 100,
'locale': locale.id,
}

# User should get an error trying to publish on Release
Expand Down

0 comments on commit 82777da

Please sign in to comment.