From e526b522ce775dd3b895257e973060690b6fd4ff Mon Sep 17 00:00:00 2001 From: Mike Cooper Date: Thu, 18 Feb 2016 13:21:19 -0800 Subject: [PATCH] Make Recipe locale and country selection many-to-many Fixes bug 1248263. --- normandy/recipes/admin.py | 28 ++++++++-- .../migrations/0016_auto_20160218_2024.py | 55 +++++++++++++++++++ normandy/recipes/migrations/0017_countries.py | 49 +++++++++++++++++ normandy/recipes/models.py | 21 +++++-- normandy/recipes/storage.py | 6 ++ 5 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 normandy/recipes/migrations/0016_auto_20160218_2024.py create mode 100644 normandy/recipes/migrations/0017_countries.py diff --git a/normandy/recipes/admin.py b/normandy/recipes/admin.py index 1dd1a90a4..7addeca26 100644 --- a/normandy/recipes/admin.py +++ b/normandy/recipes/admin.py @@ -19,10 +19,16 @@ class RecipeActionInline(SortableTabularInline): @admin.register(models.Recipe) class RecipeAdmin(NonSortableParentAdmin): - list_display = ['name', 'enabled', 'locale', 'country', 'start_time', 'end_time'] - list_filter = ['enabled', 'locale', 'country'] - search_fields = ['name', 'locale', 'country'] + list_display = ['name', 'enabled', 'get_locales', 'get_countries', 'start_time', 'end_time'] + search_fields = ['name', 'locales', 'countries'] inlines = [RecipeActionInline] + filter_horizontal = ['locales', 'countries'] + + list_filter = [ + ('enabled', admin.BooleanFieldListFilter), + ('locales', admin.RelatedOnlyFieldListFilter), + ('countries', admin.RelatedOnlyFieldListFilter), + ] fieldsets = [ [None, { @@ -31,8 +37,8 @@ class RecipeAdmin(NonSortableParentAdmin): ['Delivery Rules', { 'fields': [ 'enabled', - 'locale', - 'country', + 'locales', + 'countries', 'sample_rate', 'start_time', 'end_time', @@ -40,6 +46,18 @@ class RecipeAdmin(NonSortableParentAdmin): }], ] + def get_locales(self, obj): + val = ', '.join(l.code for l in obj.locales.all()) + if not val: + val = self.get_empty_value_display() + return val + + def get_countries(self, obj): + val = ', '.join(l.name for l in obj.countries.all()) + if not val: + val = self.get_empty_value_display() + return val + @admin.register(models.Action) class ActionAdmin(admin.ModelAdmin): diff --git a/normandy/recipes/migrations/0016_auto_20160218_2024.py b/normandy/recipes/migrations/0016_auto_20160218_2024.py new file mode 100644 index 000000000..47bf1130c --- /dev/null +++ b/normandy/recipes/migrations/0016_auto_20160218_2024.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-18 20:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recipes', '0015_auto_20160217_1819'), + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=255, unique=True)), + ('name', models.CharField(max_length=255)), + ('order', models.IntegerField()), + ], + options={ + 'ordering': ['order', 'name'], + }, + ), + migrations.AddField( + model_name='locale', + name='order', + field=models.IntegerField(default=100), + preserve_default=False, + ), + migrations.RemoveField( + model_name='recipe', + name='country', + ), + migrations.RemoveField( + model_name='recipe', + name='locale', + ), + migrations.AddField( + model_name='recipe', + name='locales', + field=models.ManyToManyField(blank=True, to='recipes.Locale'), + ), + migrations.AddField( + model_name='recipe', + name='countries', + field=models.ManyToManyField(blank=True, to='recipes.Country'), + ), + migrations.AlterModelOptions( + name='locale', + options={'ordering': ['order', 'code']}, + ), + ] diff --git a/normandy/recipes/migrations/0017_countries.py b/normandy/recipes/migrations/0017_countries.py new file mode 100644 index 000000000..67050e7f1 --- /dev/null +++ b/normandy/recipes/migrations/0017_countries.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-18 19:03 +from __future__ import unicode_literals + +from django.db import migrations + +from django_countries import countries + + +def add_countries(apps, schema_editor): + Country = apps.get_model('recipes', 'Country') + for (code, name) in countries: + if code == 'US': + order = 0 + else: + order = 100 + + Country.objects.update_or_create(code=code, defaults={ + 'name': name, + 'order': order, + }) + + +def remove_countries(apps, schema_editor): + Country = apps.get_model('recipes', 'Country') + Country.objects.all().delete() + + +def set_locale_sort_order(apps, schema_editor): + Locale = apps.get_model('recipes', 'Locale') + english = Locale.objects.get(code='en-US') + english.order = 0 + english.save() + + +def noop(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('recipes', '0016_auto_20160218_2024'), + ] + + operations = [ + migrations.RunPython(add_countries, remove_countries), + migrations.RunPython(set_locale_sort_order, noop), + ] diff --git a/normandy/recipes/models.py b/normandy/recipes/models.py index 47bd30358..63df0e5cc 100644 --- a/normandy/recipes/models.py +++ b/normandy/recipes/models.py @@ -5,7 +5,6 @@ from django.db import models from adminsortable.models import SortableMixin -from django_countries.fields import CountryField from rest_framework.reverse import reverse from normandy.recipes import utils @@ -21,14 +20,28 @@ class Locale(models.Model): code = models.CharField(max_length=255, unique=True) english_name = models.CharField(max_length=255, blank=True) native_name = models.CharField(max_length=255, blank=True) + order = models.IntegerField() class Meta: - ordering = ['code'] + ordering = ['order', 'code'] def __str__(self): return '{self.code} ({self.english_name})'.format(self=self) +class Country(models.Model): + """Database table for countries from django_countries.""" + code = models.CharField(max_length=255, unique=True) + name = models.CharField(max_length=255) + order = models.IntegerField() + + class Meta: + ordering = ['order', 'name'] + + def __str__(self): + return '{self.name} ({self.code})'.format(self=self) + + class Recipe(models.Model): """A set of actions to be fetched and executed by users.""" name = models.CharField(max_length=255, unique=True) @@ -36,8 +49,8 @@ class Recipe(models.Model): # Fields that determine who this recipe is sent to. enabled = models.BooleanField(default=False) - locale = models.ForeignKey(Locale, blank=True, null=True) - country = CountryField(blank=True, null=True, default=None) + locales = models.ManyToManyField(Locale, blank=True) + countries = models.ManyToManyField(Country, blank=True) start_time = models.DateTimeField(blank=True, null=True, default=None) end_time = models.DateTimeField(blank=True, null=True, default=None) sample_rate = PercentField(default=100) diff --git a/normandy/recipes/storage.py b/normandy/recipes/storage.py index 15c51cea3..fe5652808 100644 --- a/normandy/recipes/storage.py +++ b/normandy/recipes/storage.py @@ -21,9 +21,15 @@ def update(self, name, content, last_modified): if name == 'languages.json': languages = json.loads(content) for locale_code, names in languages.items(): + if locale_code == 'en-US': + order = 0 + else: + order = 100 + Locale.objects.update_or_create(code=locale_code, defaults={ 'english_name': names['English'], 'native_name': names['native'], + 'order': order, }) # Remove obsolete locales.