diff --git a/coding_systems/icd10/migrations/0002_edition_conceptedition_concept_editions_rubric_and_more.py b/coding_systems/icd10/migrations/0002_edition_conceptedition_concept_editions_rubric_and_more.py new file mode 100644 index 000000000..dabc134c5 --- /dev/null +++ b/coding_systems/icd10/migrations/0002_edition_conceptedition_concept_editions_rubric_and_more.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.14 on 2026-05-20 21:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('icd10', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Edition', + fields=[ + ('id', models.CharField(max_length=20, primary_key=True, serialize=False)), + ('version', models.IntegerField()), + ('year', models.IntegerField()), + ('source_description', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='ConceptEdition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kind', models.CharField(choices=[('chapter', 'Chapter'), ('block', 'Block'), ('category', 'Category')], max_length=8)), + ('usage', models.CharField(choices=[('dagger', 'Dagger'), ('asterisk', 'Asterisk'), ('normal', 'Normal')], max_length=8)), + ('term', models.CharField(blank=True, max_length=255)), + ('term_modifier_4', models.CharField(blank=True, max_length=255, null=True)), + ('term_modifier_5', models.CharField(blank=True, max_length=255, null=True)), + ('concept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='concept_editions', to='icd10.concept')), + ('edition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='concept_editions', to='icd10.edition')), + ], + ), + migrations.AddField( + model_name='concept', + name='editions', + field=models.ManyToManyField(through='icd10.ConceptEdition', to='icd10.edition'), + ), + migrations.CreateModel( + name='Rubric', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kind', models.CharField(choices=[('inclusion', 'Inclusion'), ('exclusion', 'Exclusion'), ('note', 'Note'), ('text', 'Text')], max_length=9)), + ('text', models.TextField()), + ('concept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rubrics', to='icd10.concept')), + ], + ), + migrations.AddField( + model_name='conceptedition', + name='rubrics', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='concepts', to='icd10.rubric'), + ), + migrations.CreateModel( + name='DaggerAsteriskRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('asterisk_concept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dagger_relations', to='icd10.conceptedition')), + ('dagger_concept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='asterisk_relations', to='icd10.conceptedition')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('dagger_concept', 'asterisk_concept'), name='dagger_asterisk_uq_together'), models.CheckConstraint(condition=models.Q(('dagger_concept__edition_id', models.F('asterisk_concept__edition_id'))), name='dagger_asterisk_same_edition')], + }, + ), + migrations.AddConstraint( + model_name='conceptedition', + constraint=models.UniqueConstraint(fields=('concept', 'edition'), name='concept_edition_uq_together'), + ), + ] diff --git a/coding_systems/icd10/migrations/0003_migrate_existing_data_to_transitional_model.py b/coding_systems/icd10/migrations/0003_migrate_existing_data_to_transitional_model.py new file mode 100644 index 000000000..f1df30a0b --- /dev/null +++ b/coding_systems/icd10/migrations/0003_migrate_existing_data_to_transitional_model.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.14 on 2026-05-20 15:51 + +from django.db import migrations +from coding_systems.icd10.models import Concept, ConceptEdition, ConceptKind, ConceptUsage, Edition +from coding_systems.versioning.models import CodingSystemRelease + + +def move_concept_fields_to_conceptedition(apps, schema_editor): + for csr in CodingSystemRelease.objects.filter(coding_system_id="icd10"): + previous_2019_edition = Edition.objects.using(csr.database_alias).create( + id="2019-covid-no-mods", + version="20190101", + year="2019", + source_description=""", + Loaded in ClaML format from icd website. + No modifiers, rubrics, or dagger/asterisk pairs.""", + ) + ConceptEdition.objects.using(csr.database_alias).bulk_create( + [ + ConceptEdition( + concept=c, + edition=previous_2019_edition, + kind=ConceptKind(c.kind), + term=c.term, + usage=ConceptUsage.NORMAL, + ) + for c in Concept.objects.using(csr.database_alias).all() + ] + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("icd10", "0002_edition_conceptedition_concept_editions_rubric_and_more"), + ] + + operations = [ + migrations.RunPython( + move_concept_fields_to_conceptedition, + reverse_code=migrations.RunPython.noop, + ) + ] diff --git a/coding_systems/icd10/migrations/0004_remove_concept_kind_remove_concept_term.py b/coding_systems/icd10/migrations/0004_remove_concept_kind_remove_concept_term.py new file mode 100644 index 000000000..cce599dca --- /dev/null +++ b/coding_systems/icd10/migrations/0004_remove_concept_kind_remove_concept_term.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.14 on 2026-05-20 21:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('icd10', '0003_migrate_existing_data_to_transitional_model'), + ] + + operations = [ + migrations.RemoveField( + model_name='concept', + name='kind', + ), + migrations.RemoveField( + model_name='concept', + name='term', + ), + ] diff --git a/coding_systems/icd10/models.py b/coding_systems/icd10/models.py index ce9860919..e849408e0 100644 --- a/coding_systems/icd10/models.py +++ b/coding_systems/icd10/models.py @@ -1,10 +1,97 @@ from django.db import models +def get_char_choices_field(choices): + def longest_choice(choices): + return max(len(c) for c in choices.__dict__ if not c.startswith("_")) + + return models.CharField(max_length=longest_choice(choices), choices=choices) + + +class ConceptKind(models.TextChoices): + CHAPTER = "chapter" + BLOCK = "block" + CATEGORY = "category" + + +class ConceptUsage(models.TextChoices): + DAGGER = "dagger" + ASTERISK = "asterisk" + NORMAL = "normal" + + +class RubricKind(models.TextChoices): + INCLUSION = "inclusion" + EXCLUSION = "exclusion" + NOTE = "note" + TEXT = "text" + + +class Edition(models.Model): + id = models.CharField(primary_key=True, max_length=20) + version = models.IntegerField() + year = models.IntegerField() + source_description = models.CharField(max_length=255) + + class Concept(models.Model): code = models.CharField(primary_key=True, max_length=7) - kind = models.CharField(max_length=len("category")) - term = models.CharField(max_length=200) parent = models.ForeignKey( "Concept", on_delete=models.CASCADE, related_name="children", null=True ) + editions = models.ManyToManyField(Edition, through="ConceptEdition") + + +class Rubric(models.Model): + kind = get_char_choices_field(RubricKind) + text = models.TextField() + concept = models.ForeignKey( + Concept, on_delete=models.CASCADE, related_name="rubrics" + ) + + +class ConceptEdition(models.Model): + concept = models.ForeignKey( + Concept, on_delete=models.CASCADE, related_name="concept_editions" + ) + edition = models.ForeignKey( + Edition, on_delete=models.CASCADE, related_name="concept_editions" + ) + kind = get_char_choices_field(ConceptKind) + usage = get_char_choices_field(ConceptUsage) + term = models.CharField(max_length=255, blank=True) + term_modifier_4 = models.CharField(max_length=255, blank=True, null=True) + term_modifier_5 = models.CharField(max_length=255, blank=True, null=True) + rubrics = models.ForeignKey( + Rubric, on_delete=models.CASCADE, related_name="concepts" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("concept", "edition"), name="concept_edition_uq_together" + ) + ] + + +class DaggerAsteriskRelation(models.Model): + dagger_concept = models.ForeignKey( + ConceptEdition, on_delete=models.CASCADE, related_name="asterisk_relations" + ) + asterisk_concept = models.ForeignKey( + ConceptEdition, on_delete=models.CASCADE, related_name="dagger_relations" + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("dagger_concept", "asterisk_concept"), + name="dagger_asterisk_uq_together", + ), + models.CheckConstraint( + condition=models.Q( + dagger_concept__edition_id=models.F("asterisk_concept__edition_id") + ), + name="dagger_asterisk_same_edition", + ), + ]