diff --git a/nucleus/rna/admin.py b/nucleus/rna/admin.py index 7fb096ca..5f3fdc68 100644 --- a/nucleus/rna/admin.py +++ b/nucleus/rna/admin.py @@ -32,7 +32,7 @@ class Meta: class NoteAdmin(admin.ModelAdmin): form = NoteAdminForm - filter_horizontal = ["releases"] + filter_horizontal = ["releases", "relevant_countries"] list_display = ("id", "bug", "tag", "note", "created") list_display_links = ("id",) list_filter = ("tag", "is_known_issue", "releases__product", "releases__version", "progressive_rollout") @@ -148,5 +148,13 @@ def set_to_private(self, request, queryset): obj.save() +class CountryAdmin(admin.ModelAdmin): + search_fields = ("name", "code") + + class Meta: + model = models.Country + + +admin.site.register(models.Country, CountryAdmin) admin.site.register(models.Note, NoteAdmin) admin.site.register(models.Release, ReleaseAdmin) diff --git a/nucleus/rna/migrations/0008_add_country_model.py b/nucleus/rna/migrations/0008_add_country_model.py new file mode 100644 index 00000000..457d2a79 --- /dev/null +++ b/nucleus/rna/migrations/0008_add_country_model.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.23 on 2023-11-23 13:24 + +from django.db import migrations, models + +import django_extensions.db.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("rna", "0007_release_reviewed_by_release_manager"), + ] + + operations = [ + migrations.CreateModel( + name="Country", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created", django_extensions.db.fields.CreationDateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(blank=True, db_index=True, editable=False)), + ("name", models.CharField(max_length=128)), + ("code", models.CharField(help_text="3166-1-alpha-2 code", max_length=2)), + ], + ), + migrations.AddConstraint( + model_name="country", + constraint=models.UniqueConstraint(fields=("name", "code"), name="unique_country_name_for_code"), + ), + ] diff --git a/nucleus/rna/migrations/0009_data_bootstrap_countries_and_codes.py b/nucleus/rna/migrations/0009_data_bootstrap_countries_and_codes.py new file mode 100644 index 00000000..5157c7e8 --- /dev/null +++ b/nucleus/rna/migrations/0009_data_bootstrap_countries_and_codes.py @@ -0,0 +1,279 @@ +# Generated by Django 3.2.23 on 2023-11-23 13:24 + +from django.db import migrations +from django.utils.timezone import now + +iso_3166_1_alpha_2_country_codes = ( + # Source https://datahub.io/core/country-list + # Public domain license: https://opendatacommons.org/licenses/pddl/ + ("Afghanistan", "AF"), + ("Åland Islands", "AX"), + ("Albania", "AL"), + ("Algeria", "DZ"), + ("American Samoa", "AS"), + ("Andorra", "AD"), + ("Angola", "AO"), + ("Anguilla", "AI"), + ("Antarctica", "AQ"), + ("Antigua and Barbuda", "AG"), + ("Argentina", "AR"), + ("Armenia", "AM"), + ("Aruba", "AW"), + ("Australia", "AU"), + ("Austria", "AT"), + ("Azerbaijan", "AZ"), + ("Bahamas", "BS"), + ("Bahrain", "BH"), + ("Bangladesh", "BD"), + ("Barbados", "BB"), + ("Belarus", "BY"), + ("Belgium", "BE"), + ("Belize", "BZ"), + ("Benin", "BJ"), + ("Bermuda", "BM"), + ("Bhutan", "BT"), + ("Bolivia, Plurinational State of", "BO"), + ("Bonaire, Sint Eustatius and Saba", "BQ"), + ("Bosnia and Herzegovina", "BA"), + ("Botswana", "BW"), + ("Bouvet Island", "BV"), + ("Brazil", "BR"), + ("British Indian Ocean Territory", "IO"), + ("Brunei Darussalam", "BN"), + ("Bulgaria", "BG"), + ("Burkina Faso", "BF"), + ("Burundi", "BI"), + ("Cambodia", "KH"), + ("Cameroon", "CM"), + ("Canada", "CA"), + ("Cape Verde", "CV"), + ("Cayman Islands", "KY"), + ("Central African Republic", "CF"), + ("Chad", "TD"), + ("Chile", "CL"), + ("China", "CN"), + ("Christmas Island", "CX"), + ("Cocos (Keeling) Islands", "CC"), + ("Colombia", "CO"), + ("Comoros", "KM"), + ("Congo", "CG"), + ("Congo, the Democratic Republic of the", "CD"), + ("Cook Islands", "CK"), + ("Costa Rica", "CR"), + ("Côte d'Ivoire", "CI"), + ("Croatia", "HR"), + ("Cuba", "CU"), + ("Curaçao", "CW"), + ("Cyprus", "CY"), + ("Czech Republic", "CZ"), + ("Denmark", "DK"), + ("Djibouti", "DJ"), + ("Dominica", "DM"), + ("Dominican Republic", "DO"), + ("Ecuador", "EC"), + ("Egypt", "EG"), + ("El Salvador", "SV"), + ("Equatorial Guinea", "GQ"), + ("Eritrea", "ER"), + ("Estonia", "EE"), + ("Ethiopia", "ET"), + ("Falkland Islands (Malvinas)", "FK"), + ("Faroe Islands", "FO"), + ("Fiji", "FJ"), + ("Finland", "FI"), + ("France", "FR"), + ("French Guiana", "GF"), + ("French Polynesia", "PF"), + ("French Southern Territories", "TF"), + ("Gabon", "GA"), + ("Gambia", "GM"), + ("Georgia", "GE"), + ("Germany", "DE"), + ("Ghana", "GH"), + ("Gibraltar", "GI"), + ("Greece", "GR"), + ("Greenland", "GL"), + ("Grenada", "GD"), + ("Guadeloupe", "GP"), + ("Guam", "GU"), + ("Guatemala", "GT"), + ("Guernsey", "GG"), + ("Guinea", "GN"), + ("Guinea-Bissau", "GW"), + ("Guyana", "GY"), + ("Haiti", "HT"), + ("Heard Island and McDonald Islands", "HM"), + ("Holy See (Vatican City State)", "VA"), + ("Honduras", "HN"), + ("Hong Kong", "HK"), + ("Hungary", "HU"), + ("Iceland", "IS"), + ("India", "IN"), + ("Indonesia", "ID"), + ("Iran, Islamic Republic of", "IR"), + ("Iraq", "IQ"), + ("Ireland", "IE"), + ("Isle of Man", "IM"), + ("Israel", "IL"), + ("Italy", "IT"), + ("Jamaica", "JM"), + ("Japan", "JP"), + ("Jersey", "JE"), + ("Jordan", "JO"), + ("Kazakhstan", "KZ"), + ("Kenya", "KE"), + ("Kiribati", "KI"), + ("Korea, Democratic People's Republic of", "KP"), + ("Korea, Republic of", "KR"), + ("Kuwait", "KW"), + ("Kyrgyzstan", "KG"), + ("Lao People's Democratic Republic", "LA"), + ("Latvia", "LV"), + ("Lebanon", "LB"), + ("Lesotho", "LS"), + ("Liberia", "LR"), + ("Libya", "LY"), + ("Liechtenstein", "LI"), + ("Lithuania", "LT"), + ("Luxembourg", "LU"), + ("Macao", "MO"), + ("Macedonia, the Former Yugoslav Republic of", "MK"), + ("Madagascar", "MG"), + ("Malawi", "MW"), + ("Malaysia", "MY"), + ("Maldives", "MV"), + ("Mali", "ML"), + ("Malta", "MT"), + ("Marshall Islands", "MH"), + ("Martinique", "MQ"), + ("Mauritania", "MR"), + ("Mauritius", "MU"), + ("Mayotte", "YT"), + ("Mexico", "MX"), + ("Micronesia, Federated States of", "FM"), + ("Moldova, Republic of", "MD"), + ("Monaco", "MC"), + ("Mongolia", "MN"), + ("Montenegro", "ME"), + ("Montserrat", "MS"), + ("Morocco", "MA"), + ("Mozambique", "MZ"), + ("Myanmar", "MM"), + ("Namibia", "NA"), + ("Nauru", "NR"), + ("Nepal", "NP"), + ("Netherlands", "NL"), + ("New Caledonia", "NC"), + ("New Zealand", "NZ"), + ("Nicaragua", "NI"), + ("Niger", "NE"), + ("Nigeria", "NG"), + ("Niue", "NU"), + ("Norfolk Island", "NF"), + ("Northern Mariana Islands", "MP"), + ("Norway", "NO"), + ("Oman", "OM"), + ("Pakistan", "PK"), + ("Palau", "PW"), + ("Palestine, State of", "PS"), + ("Panama", "PA"), + ("Papua New Guinea", "PG"), + ("Paraguay", "PY"), + ("Peru", "PE"), + ("Philippines", "PH"), + ("Pitcairn", "PN"), + ("Poland", "PL"), + ("Portugal", "PT"), + ("Puerto Rico", "PR"), + ("Qatar", "QA"), + ("Réunion", "RE"), + ("Romania", "RO"), + ("Russian Federation", "RU"), + ("Rwanda", "RW"), + ("Saint Barthélemy", "BL"), + ("Saint Helena, Ascension and Tristan da Cunha", "SH"), + ("Saint Kitts and Nevis", "KN"), + ("Saint Lucia", "LC"), + ("Saint Martin (French part)", "MF"), + ("Saint Pierre and Miquelon", "PM"), + ("Saint Vincent and the Grenadines", "VC"), + ("Samoa", "WS"), + ("San Marino", "SM"), + ("Sao Tome and Principe", "ST"), + ("Saudi Arabia", "SA"), + ("Senegal", "SN"), + ("Serbia", "RS"), + ("Seychelles", "SC"), + ("Sierra Leone", "SL"), + ("Singapore", "SG"), + ("Sint Maarten (Dutch part)", "SX"), + ("Slovakia", "SK"), + ("Slovenia", "SI"), + ("Solomon Islands", "SB"), + ("Somalia", "SO"), + ("South Africa", "ZA"), + ("South Georgia and the South Sandwich Islands", "GS"), + ("South Sudan", "SS"), + ("Spain", "ES"), + ("Sri Lanka", "LK"), + ("Sudan", "SD"), + ("Suriname", "SR"), + ("Svalbard and Jan Mayen", "SJ"), + ("Swaziland", "SZ"), + ("Sweden", "SE"), + ("Switzerland", "CH"), + ("Syrian Arab Republic", "SY"), + ("Taiwan, Province of China", "TW"), + ("Tajikistan", "TJ"), + ("Tanzania, United Republic of", "TZ"), + ("Thailand", "TH"), + ("Timor-Leste", "TL"), + ("Togo", "TG"), + ("Tokelau", "TK"), + ("Tonga", "TO"), + ("Trinidad and Tobago", "TT"), + ("Tunisia", "TN"), + ("Turkey", "TR"), + ("Turkmenistan", "TM"), + ("Turks and Caicos Islands", "TC"), + ("Tuvalu", "TV"), + ("Uganda", "UG"), + ("Ukraine", "UA"), + ("United Arab Emirates", "AE"), + ("United Kingdom", "GB"), + ("United States", "US"), + ("United States Minor Outlying Islands", "UM"), + ("Uruguay", "UY"), + ("Uzbekistan", "UZ"), + ("Vanuatu", "VU"), + ("Venezuela, Bolivarian Republic of", "VE"), + ("Viet Nam", "VN"), + ("Virgin Islands, British", "VG"), + ("Virgin Islands, U.S.", "VI"), + ("Wallis and Futuna", "WF"), + ("Western Sahara", "EH"), + ("Yemen", "YE"), + ("Zambia", "ZM"), + ("Zimbabwe", "ZW"), +) + + +def forwards(apps, schema_editor): + Country = apps.get_model("rna.Country") + for name, code in iso_3166_1_alpha_2_country_codes: + Country.objects.create(name=name, code=code, modified=now()) + + +def backwards(apps, schema_editor): + Country = apps.get_model("rna.Country") + Country.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("rna", "0008_add_country_model"), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/nucleus/rna/migrations/0010_note_only_applies_to_countries.py b/nucleus/rna/migrations/0010_note_only_applies_to_countries.py new file mode 100644 index 00000000..d9bdc341 --- /dev/null +++ b/nucleus/rna/migrations/0010_note_only_applies_to_countries.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.23 on 2023-11-23 13:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("rna", "0009_data_bootstrap_countries_and_codes"), + ] + + operations = [ + migrations.AddField( + model_name="note", + name="relevant_countries", + field=models.ManyToManyField( + blank=True, + help_text=( + "Select the countries where this Note applies, as part of a " + "progressive rollout. This info will only be shown on the Release " + "page if 'Progressive rollout', above, is ticked." + ), + to="rna.Country", + ), + ), + ] diff --git a/nucleus/rna/migrations/0011_alter_country_options.py b/nucleus/rna/migrations/0011_alter_country_options.py new file mode 100644 index 00000000..7ffecc22 --- /dev/null +++ b/nucleus/rna/migrations/0011_alter_country_options.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.23 on 2023-11-23 13:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("rna", "0010_note_only_applies_to_countries"), + ] + + operations = [ + migrations.AlterModelOptions( + name="country", + options={"ordering": ["name"], "verbose_name_plural": "Countries"}, + ), + ] diff --git a/nucleus/rna/models.py b/nucleus/rna/models.py index 219f63cb..3ae1b935 100644 --- a/nucleus/rna/models.py +++ b/nucleus/rna/models.py @@ -6,7 +6,7 @@ from django.conf import settings from django.db import models -from django.db.models import Q +from django.db.models import Q, UniqueConstraint from django.forms.models import model_to_dict from django.utils.text import slugify from django.utils.timezone import now @@ -177,6 +177,15 @@ class Note(SaveToGithubModel): sort_num = models.IntegerField(default=0) is_public = models.BooleanField(default=True) progressive_rollout = models.BooleanField(default=False) + relevant_countries = models.ManyToManyField( + "Country", + blank=True, + help_text=( + "Select the countries where this Note applies, as part of a " + "progressive rollout. This info will only be shown on the Release " + "page if 'Progressive rollout', above, is ticked." + ), + ) related_field_to_github = "releases" @@ -201,6 +210,9 @@ def to_dict(self, release=None): if release and self.is_known_issue_for(release): data["tag"] = "Known" + if self.relevant_countries.count(): + data["relevant_countries"] = [x.to_dict() for x in self.relevant_countries.all()] + return data def __str__(self): @@ -208,3 +220,35 @@ def __str__(self): class Meta: get_latest_by = "modified" + + +class Country(SaveToGithubModel): + # Simple model for associating a Country with a Note + + name = models.CharField( + blank=False, + max_length=128, + ) + code = models.CharField( + blank=False, + help_text="3166-1-alpha-2 code", + max_length=2, + ) + + def __str__(self): + return f"{self.name} ({self.code})" + + class Meta: + verbose_name_plural = "Countries" + ordering = ["name"] + constraints = [ + UniqueConstraint( + fields=["name", "code"], + name="unique_country_name_for_code", + ), + ] + + def to_dict(self): + data = model_to_dict(self) + del data["id"] # no need to dump out the internal ID for a Country + return data diff --git a/nucleus/rna/serializers.py b/nucleus/rna/serializers.py index bec98780..0b82a4ac 100644 --- a/nucleus/rna/serializers.py +++ b/nucleus/rna/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers -from .models import Note, Release +from .models import Country, Note, Release class HyperlinkedModelSerializerWithPkField(serializers.HyperlinkedModelSerializer): @@ -14,7 +14,21 @@ def get_default_field_names(self, declared_fields, model_info): return fields +class CountrySerializer(serializers.ModelSerializer): + class Meta: + model = Country + fields = [ + "name", + "code", + ] + + class NoteSerializer(HyperlinkedModelSerializerWithPkField): + relevant_countries = CountrySerializer( + read_only=True, + many=True, + ) + class Meta: model = Note fields = "__all__" diff --git a/nucleus/rna/tests/test_models.py b/nucleus/rna/tests/test_models.py index 182cb653..9e9e0679 100644 --- a/nucleus/rna/tests/test_models.py +++ b/nucleus/rna/tests/test_models.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.utils.timezone import now -from nucleus.rna.models import Note, Release +from nucleus.rna.models import Country, Note, Release class TestReleaseQueries(TestCase): @@ -103,3 +103,35 @@ def test_to_dict__simple__no_relations(self): assert "created" in dumped_dict assert "modified" in dumped_dict + + def test_to_dict__relevant_country_field(self): + # Country data is bootstrapped by a data migration + + iceland = Country.objects.get(code="IS") + india = Country.objects.get(code="IN") + + data = dict( + bug=1234, + note="Test note", + tag="this is a tag", + sort_num=1, + is_public=True, + progressive_rollout=True, + ) + + note = Note(**data) + note.save() + + assert note.relevant_countries.count() == 0 + dumped_dict = note.to_dict() + assert dumped_dict["relevant_countries"] == [] + + note.relevant_countries.add(india) + note.relevant_countries.add(iceland) + assert note.relevant_countries.count() == 2 + + dumped_dict = note.to_dict() + assert dumped_dict["relevant_countries"] == [ + {"name": "Iceland", "code": "IS"}, + {"name": "India", "code": "IN"}, + ]