From c52d4e736dda7943666d7412ea95f553e8e89902 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Sat, 3 Mar 2018 12:10:17 -0700 Subject: [PATCH] ENH: Sex fields as lookups (#55) --- ccdb/api/urls.py | 1 + ccdb/species/admin.py | 11 ++- ccdb/species/migrations/0004_sex.py | 39 +++++++++++ .../migrations/0005_replace_sex_fields.py | 68 +++++++++++++++++++ ccdb/species/models.py | 18 ++++- ccdb/species/serializers.py | 9 ++- ccdb/species/tests/factories.py | 14 +++- ccdb/species/viewsets.py | 10 ++- .../migrations/0004_auto_20180228_1246.py | 21 ++++++ 9 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 ccdb/species/migrations/0004_sex.py create mode 100644 ccdb/species/migrations/0005_replace_sex_fields.py create mode 100644 ccdb/users/migrations/0004_auto_20180228_1246.py diff --git a/ccdb/api/urls.py b/ccdb/api/urls.py index bd300a4..cb14947 100644 --- a/ccdb/api/urls.py +++ b/ccdb/api/urls.py @@ -35,6 +35,7 @@ router.register(r'sites', locations_viewsets.SiteViewSet) router.register(r'study-locations', locations_viewsets.StudyLocationViewSet) # Species router.register(r'species', species_viewsets.SpeciesViewSet) +router.register(r'sexes', species_viewsets.SexViewSet) router.register(r'collection-species', species_viewsets.CollectionSpeciesViewSet) diff --git a/ccdb/species/admin.py b/ccdb/species/admin.py index cbee00e..936fba0 100644 --- a/ccdb/species/admin.py +++ b/ccdb/species/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Species, TrapSpecies, CollectionSpecies +from .models import Species, Sex, TrapSpecies, CollectionSpecies class SpeciesAdmin(admin.ModelAdmin): @@ -12,6 +12,14 @@ class SpeciesAdmin(admin.ModelAdmin): fields = ('common_name', 'genus', 'species', 'parasite', 'sort_order') +class SexAdmin(admin.ModelAdmin): + list_display = ('name', 'sort_order') + list_display_links = ('name',) + search_fields = ('name',) + list_per_page = 25 + fields = ('name', 'sort_order') + + class TrapSpeciesAdmin(admin.ModelAdmin): list_display = ('collection_trap', 'species', 'sex', 'count', 'count_estimated') @@ -32,5 +40,6 @@ class CollectionSpeciesAdmin(admin.ModelAdmin): admin.site.register(Species, SpeciesAdmin) +admin.site.register(Sex, SexAdmin) admin.site.register(TrapSpecies, TrapSpeciesAdmin) admin.site.register(CollectionSpecies, CollectionSpeciesAdmin) diff --git a/ccdb/species/migrations/0004_sex.py b/ccdb/species/migrations/0004_sex.py new file mode 100644 index 0000000..2a793b0 --- /dev/null +++ b/ccdb/species/migrations/0004_sex.py @@ -0,0 +1,39 @@ +from django.db import migrations, models +from django.forms import modelform_factory + + +class Migration(migrations.Migration): + def migrate(apps, schema_editor): + Sex = apps.get_model('species', 'Sex') + SexForm = modelform_factory(Sex, fields=('name', 'sort_order')) + for i, s in enumerate(['male', 'female', 'mixed', 'unknown']): + form = SexForm(dict(name=s, sort_order=i)) + if form.is_valid(): + form.save() + else: + print('sex', form.errors.as_data()) + + def rollback(apps, schema_editor): + Sex = apps.get_model('species', 'Sex') + Sex.objects.all().delete() + + dependencies = [ + ('species', '0003_DATA_reset_sequences'), + ] + + operations = [ + migrations.CreateModel( + name='Sex', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, + serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=25, unique=True)), + ('sort_order', models.IntegerField(blank=True, null=True)), + ], + options={ + 'verbose_name_plural': 'sex', + 'ordering': ['sort_order'], + }, + ), + migrations.RunPython(migrate, rollback), + ] diff --git a/ccdb/species/migrations/0005_replace_sex_fields.py b/ccdb/species/migrations/0005_replace_sex_fields.py new file mode 100644 index 0000000..e634e30 --- /dev/null +++ b/ccdb/species/migrations/0005_replace_sex_fields.py @@ -0,0 +1,68 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + def migrate(apps, schema_editor): + Sex = apps.get_model('species', 'Sex') + CollectionSpecies = apps.get_model('species', 'CollectionSpecies') + + for cs in CollectionSpecies.objects.all(): + if cs.old_sex: + if cs.old_sex == 'both': + s = 'mixed' + elif cs.old_sex not in ['male', 'female', 'mixed', 'unknown']: + s = 'unknown' + else: + s = cs.old_sex + cs.sex = Sex.objects.get(name=s) + cs.save() + + def rollback(apps, schema_editor): + CollectionSpecies = apps.get_model('species', 'CollectionSpecies') + + for cs in CollectionSpecies.objects.all(): + if cs.sex: + cs.sex = None + cs.old_sex = '' + cs.save() + + dependencies = [ + ('species', '0004_sex'), + ] + + operations = [ + migrations.RenameField( + model_name='collectionspecies', + old_name='sex', + new_name='old_sex', + ), + migrations.RenameField( + model_name='trapspecies', + old_name='sex', + new_name='old_sex', + ), + migrations.AddField( + model_name='collectionspecies', + name='sex', + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='collection_species', to='species.Sex'), + ), + migrations.AddField( + model_name='trapspecies', + name='sex', + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='trap_species', to='species.Sex'), + ), + migrations.RunPython(migrate, rollback), + migrations.RemoveField( + model_name='collectionspecies', + name='old_sex', + ), + migrations.RemoveField( + model_name='trapspecies', + name='old_sex', + ), + ] diff --git a/ccdb/species/models.py b/ccdb/species/models.py index fd52285..3955947 100644 --- a/ccdb/species/models.py +++ b/ccdb/species/models.py @@ -17,13 +17,26 @@ class Species(models.Model): verbose_name_plural = 'species' +class Sex(models.Model): + name = models.CharField(max_length=25, unique=True) + sort_order = models.IntegerField(blank=True, null=True) + + def __str__(self): + return self.name + + class Meta: + ordering = ['sort_order'] + verbose_name_plural = 'sex' + + class TrapSpecies(models.Model): collection_trap = models.ForeignKey('collections_ccdb.CollectionTrap', related_name='trap_species', on_delete=models.CASCADE) species = models.ForeignKey(Species, related_name='trap_species', on_delete=models.CASCADE) - sex = models.CharField(max_length=25, blank=True) + sex = models.ForeignKey(Sex, related_name='trap_species', + on_delete=models.CASCADE, null=True) count = models.IntegerField(blank=True, null=True) count_estimated = models.BooleanField(default=False) @@ -40,7 +53,8 @@ class CollectionSpecies(models.Model): on_delete=models.CASCADE) species = models.ForeignKey(Species, related_name='collection_species', on_delete=models.CASCADE) - sex = models.CharField(max_length=25, blank=True) + sex = models.ForeignKey(Sex, related_name='collection_species', + on_delete=models.CASCADE, null=True) count = models.IntegerField(blank=True, null=True) count_estimated = models.BooleanField(default=False) diff --git a/ccdb/species/serializers.py b/ccdb/species/serializers.py index 2e1c874..3874249 100644 --- a/ccdb/species/serializers.py +++ b/ccdb/species/serializers.py @@ -1,6 +1,6 @@ from rest_framework_json_api import serializers -from .models import Species, CollectionSpecies +from .models import Species, Sex, CollectionSpecies class SpeciesSerializer(serializers.ModelSerializer): @@ -9,10 +9,17 @@ class SpeciesSerializer(serializers.ModelSerializer): fields = ('common_name', 'genus', 'species', 'parasite', 'sort_order') +class SexSerializer(serializers.ModelSerializer): + class Meta: + model = Sex + fields = ('name', 'sort_order') + + class CollectionSpeciesSerializer(serializers.ModelSerializer): included_serializers = { 'collection': 'ccdb.collections_ccdb.serializers.CollectionSerializer', 'species': 'ccdb.species.serializers.SpeciesSerializer', + 'sex': 'ccdb.species.serializers.SexSerializer', } class Meta: diff --git a/ccdb/species/tests/factories.py b/ccdb/species/tests/factories.py index 3f8ae30..2f02af3 100644 --- a/ccdb/species/tests/factories.py +++ b/ccdb/species/tests/factories.py @@ -1,7 +1,7 @@ from factory import DjangoModelFactory, Sequence, SubFactory from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyInteger -from ..models import Species, TrapSpecies, CollectionSpecies +from ..models import Species, Sex, TrapSpecies, CollectionSpecies from ccdb.collections_ccdb.tests.factories import (CollectionFactory, CollectionTrapFactory) @@ -17,13 +17,21 @@ class SpeciesFactory(DjangoModelFactory): sort_order = Sequence(lambda n: n) +class SexFactory(DjangoModelFactory): + class Meta: + model = Sex + + name = Sequence(lambda n: 'sex{}'.format(n)) + sort_order = Sequence(lambda n: n) + + class TrapSpeciesFactory(DjangoModelFactory): class Meta: model = TrapSpecies collection_trap = SubFactory(CollectionTrapFactory) species = SubFactory(SpeciesFactory) - sex = FuzzyText(length=25) + sex = SubFactory(SexFactory) count = FuzzyInteger(0) count_estimated = FuzzyChoice(choices=[True, False]) @@ -34,6 +42,6 @@ class CollectionSpeciesFactory(DjangoModelFactory): collection = SubFactory(CollectionFactory) species = SubFactory(SpeciesFactory) - sex = FuzzyText(length=25) + sex = SubFactory(SexFactory) count = FuzzyInteger(0) count_estimated = FuzzyChoice(choices=[True, False]) diff --git a/ccdb/species/viewsets.py b/ccdb/species/viewsets.py index 35d4097..6e610c6 100644 --- a/ccdb/species/viewsets.py +++ b/ccdb/species/viewsets.py @@ -1,7 +1,8 @@ from rest_framework import viewsets -from .models import Species, CollectionSpecies -from .serializers import SpeciesSerializer, CollectionSpeciesSerializer +from .models import Species, Sex, CollectionSpecies +from .serializers import (SpeciesSerializer, SexSerializer, + CollectionSpeciesSerializer) class SpeciesViewSet(viewsets.ModelViewSet): @@ -9,6 +10,11 @@ class SpeciesViewSet(viewsets.ModelViewSet): serializer_class = SpeciesSerializer +class SexViewSet(viewsets.ModelViewSet): + queryset = Sex.objects.all() + serializer_class = SexSerializer + + class CollectionSpeciesViewSet(viewsets.ModelViewSet): queryset = CollectionSpecies.objects.all() serializer_class = CollectionSpeciesSerializer diff --git a/ccdb/users/migrations/0004_auto_20180228_1246.py b/ccdb/users/migrations/0004_auto_20180228_1246.py new file mode 100644 index 0000000..183616a --- /dev/null +++ b/ccdb/users/migrations/0004_auto_20180228_1246.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.9 on 2018-02-28 12:46 +from __future__ import unicode_literals + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_remove_user_timezone'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ]