Commit b532f9a1 authored by Nicolas Noé's avatar Nicolas Noé
Browse files

Basic taxonomy management.

parent 4fe4bf74
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms
from .models import Specimen, SpecimenLocation, Person, Fixation, Station, Expedition, SpecimenPicture
from mptt.admin import DraggableMPTTAdmin
from .models import Specimen, SpecimenLocation, Person, Fixation, Station, Expedition, SpecimenPicture, Taxon
from .widgets import LatLongWidget from .widgets import LatLongWidget
...@@ -66,4 +69,8 @@ class ExpeditionAdmin(admin.ModelAdmin): ...@@ -66,4 +69,8 @@ class ExpeditionAdmin(admin.ModelAdmin):
class SpecimenPictureAdmin(admin.ModelAdmin): class SpecimenPictureAdmin(admin.ModelAdmin):
fields = ('specimen', 'image', 'high_interest') fields = ('specimen', 'image', 'high_interest')
@admin.register(Taxon)
class TaxonAdmin(DraggableMPTTAdmin):
pass
admin.site.site_header = 'Astapor administration' admin.site.site_header = 'Astapor administration'
\ No newline at end of file
...@@ -11,7 +11,6 @@ from specimens.models import Person, SpecimenLocation, Specimen, Fixation, Exped ...@@ -11,7 +11,6 @@ from specimens.models import Person, SpecimenLocation, Specimen, Fixation, Exped
MODELS_TO_TRUNCATE = [Station, Expedition, Fixation, Person, SpecimenLocation, Specimen] MODELS_TO_TRUNCATE = [Station, Expedition, Fixation, Person, SpecimenLocation, Specimen]
# TODO: document use of this script: # TODO: document use of this script:
# - export Google Sheet (specimens) as CSV (separator: comma)
# - Column name is important, not column order # - Column name is important, not column order
# - Lat/lon use comma as a separator # - Lat/lon use comma as a separator
...@@ -35,7 +34,7 @@ def get_or_create_station_and_expedition(station_name, expedition_name): ...@@ -35,7 +34,7 @@ def get_or_create_station_and_expedition(station_name, expedition_name):
class Command(BaseCommand): class Command(BaseCommand):
help = 'Initial data import to populate the tables' help = 'Import specimens from a CSV file and attach them to the existing taxonomy.'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('csv_file') parser.add_argument('csv_file')
......
import csv
from django.core.management.base import BaseCommand
from specimens.models import TaxonRank, Taxon, TaxonStatus, SPECIES_RANK_NAME, SUBGENUS_RANK_NAME
MODELS_TO_TRUNCATE = [Taxon, TaxonRank, TaxonStatus]
def create_initial_ranks():
TaxonRank.objects.bulk_create([
TaxonRank(name='Kingdom'),
TaxonRank(name='Phylum'),
TaxonRank(name='Class'),
TaxonRank(name='Order'),
TaxonRank(name='Family'),
TaxonRank(name='Genus'),
TaxonRank(name=SUBGENUS_RANK_NAME),
TaxonRank(name=SPECIES_RANK_NAME)
])
class Command(BaseCommand):
help = 'Import taxonomy from a CSV file.'
def add_arguments(self, parser):
parser.add_argument('csv_file')
parser.add_argument(
'--truncate',
action='store_true',
dest='truncate',
default=False,
help='Truncate all tables prior to import',
)
def handle(self, *args, **options):
self.stdout.write('Importing data from file...')
with open(options['csv_file']) as csv_file:
if options['truncate']:
for model in MODELS_TO_TRUNCATE:
self.stdout.write('Truncate model {name} ...'.format(name=model.__name__), ending='')
model.objects.all().delete()
self.stdout.write(self.style.SUCCESS('OK'))
self.stdout.write('Creating initial ranks...')
create_initial_ranks()
for i, row in enumerate(csv.DictReader(csv_file, delimiter=',')):
self.stdout.write('Processing row #{i}...'.format(i=i), ending='')
species_status, _ = TaxonStatus.objects.get_or_create(name=row['Status'])
# Starting from the higher ranks
kingdom, _ = Taxon.objects.get_or_create(name=row['Kingdom'].strip(),
rank=TaxonRank.objects.get(name="Kingdom"))
phylum, _ = Taxon.objects.get_or_create(name=row['Phylum'].strip(),
rank=TaxonRank.objects.get(name="Phylum"),
parent=kingdom)
class_taxon, _ = Taxon.objects.get_or_create(name=row['Class'].strip(),
rank=TaxonRank.objects.get(name="Class"),
parent=phylum)
order_taxon, _ = Taxon.objects.get_or_create(name=row['Order'].strip(),
rank=TaxonRank.objects.get(name="Order"),
parent=class_taxon)
family, _ = Taxon.objects.get_or_create(name=row['Family'].strip(),
rank=TaxonRank.objects.get(name="Family"),
parent=order_taxon)
genus, _ = Taxon.objects.get_or_create(name=row['Genus'].strip(),
rank=TaxonRank.objects.get(name="Genus"),
parent=family)
# Subgenus rank is optional
subgenus_source = row['Subgenus'].strip()
if subgenus_source:
subgenus, _ = Taxon.objects.get_or_create(name=subgenus_source,
rank=TaxonRank.objects.get(name=SUBGENUS_RANK_NAME),
parent=genus)
species_parent = subgenus if subgenus_source else genus
species, _ = Taxon.objects.get_or_create(name=row['Species'].strip(),
rank=TaxonRank.objects.get(name=SPECIES_RANK_NAME),
parent=species_parent,
status=species_status,
aphia_id=row['Aphia_ID'].strip(),
authority=row['Authority'].strip())
self.stdout.write(self.style.SUCCESS('OK'))
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-21 07:43 # Generated by Django 1.11.2 on 2017-06-22 09:08
from __future__ import unicode_literals from __future__ import unicode_literals
import django.contrib.gis.db.models.fields import django.contrib.gis.db.models.fields
import django.contrib.postgres.fields.ranges import django.contrib.postgres.fields.ranges
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
...@@ -78,6 +79,47 @@ class Migration(migrations.Migration): ...@@ -78,6 +79,47 @@ class Migration(migrations.Migration):
('expedition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='specimens.Expedition')), ('expedition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='specimens.Expedition')),
], ],
), ),
migrations.CreateModel(
name='Taxon',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('aphia_id', models.IntegerField(blank=True, null=True)),
('authority', models.CharField(blank=True, max_length=100, null=True)),
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(db_index=True, editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='specimens.Taxon')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='TaxonRank',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='TaxonStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.AddField(
model_name='taxon',
name='rank',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='specimens.TaxonRank'),
),
migrations.AddField(
model_name='taxon',
name='status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='specimens.TaxonStatus'),
),
migrations.AddField( migrations.AddField(
model_name='specimen', model_name='specimen',
name='specimen_location', name='specimen_location',
......
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.fields import FloatRangeField from django.contrib.postgres.fields import FloatRangeField
from mptt.models import MPTTModel, TreeForeignKey
UNKNOWN_STATION_NAME = '<Unknown>' # Sometimes we need a "fake" station to link Specimen to Expedition UNKNOWN_STATION_NAME = '<Unknown>' # Sometimes we need a "fake" station to link Specimen to Expedition
# Ranks: Sometimes names are used as an identifier...
SPECIES_RANK_NAME = "Species"
SUBGENUS_RANK_NAME = "Subgenus"
class TaxonRank(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class TaxonStatus(models.Model):
name = models.CharField(max_length=100)
class Taxon(MPTTModel):
name = models.CharField(max_length=100)
rank = models.ForeignKey(TaxonRank)
status = models.ForeignKey(TaxonStatus, null=True, blank=True)
aphia_id = models.IntegerField(null=True, blank=True)
authority = models.CharField(max_length=100, null=True, blank=True)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
def is_species(self):
return self.rank.name == SPECIES_RANK_NAME
def is_subgenus(self):
return self.rank.name == SUBGENUS_RANK_NAME
def __str__(self):
# Specific representation for Species
if self.is_species():
if self.parent.is_subgenus():
name = "{genus_name} ({subgenus_name}) {species_name}".format(species_name=self.name,
subgenus_name=self.parent.name,
genus_name=self.parent.parent.name)
else:
name = "{genus_name} {species_name} ".format(species_name=self.name, genus_name=self.parent.name)
else:
name = self.name
return "{name} [{rank}]".format(name=name, rank=self.rank)
class MPTTMeta:
order_insertion_by = ['name']
class Person(models.Model): class Person(models.Model):
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100)
......
...@@ -37,6 +37,7 @@ INSTALLED_APPS = [ ...@@ -37,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.gis', 'django.contrib.gis',
'export_action', 'export_action',
'specimens', 'specimens',
'mptt',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment