From 538efb0747d88bcc044c385938a1185ca0d1bcfd Mon Sep 17 00:00:00 2001 From: "db@duet5i.wsl.arch" Date: Tue, 4 Aug 2026 18:10:28 +0300 Subject: [PATCH] Add unmanaged curriculum read models --- core/models.py | 60 ++++++++++++++++++++++++- tests/test_curriculum_models.py | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/test_curriculum_models.py diff --git a/core/models.py b/core/models.py index fd18c6e..36defe0 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,61 @@ from django.db import models -# Create your models here. + +class Concept(models.Model): + slug = models.TextField(primary_key=True) + + class Meta: + managed = False + db_table = "concept" + + def __str__(self) -> str: + return self.slug + + +class RelationType(models.Model): + slug = models.TextField(primary_key=True) + + class Meta: + managed = False + db_table = "relation_type" + + def __str__(self) -> str: + return self.slug + + +class Relation(models.Model): + id = models.UUIDField(primary_key=True) + + source = models.ForeignKey( + Concept, + on_delete=models.CASCADE, + db_column="source", + related_name="outgoing_relations", + ) + + type = models.ForeignKey( + RelationType, + on_delete=models.RESTRICT, + db_column="type", + related_name="relations", + ) + + target = models.ForeignKey( + Concept, + on_delete=models.CASCADE, + db_column="target", + related_name="incoming_relations", + ) + + class Meta: + managed = False + db_table = "relation" + constraints = [ + models.UniqueConstraint( + fields=("source", "type", "target"), + name="unique_typed_relation", + ), + ] + + def __str__(self) -> str: + return f"{self.source_id} —{self.type_id}→ {self.target_id}" diff --git a/tests/test_curriculum_models.py b/tests/test_curriculum_models.py new file mode 100644 index 0000000..e73495f --- /dev/null +++ b/tests/test_curriculum_models.py @@ -0,0 +1,77 @@ +import uuid + +from django.db.models.deletion import CASCADE, RESTRICT + +from core.models import Concept, Relation, RelationType + + +def test_concept_uses_slug_as_primary_key(): + primary_key = Concept._meta.pk + + assert primary_key.name == "slug" + assert primary_key.column == "slug" + + +def test_relation_type_uses_slug_as_primary_key(): + primary_key = RelationType._meta.pk + + assert primary_key.name == "slug" + assert primary_key.column == "slug" + + +def test_relation_maps_expected_database_columns(): + assert Relation._meta.pk.name == "id" + assert Relation._meta.get_field("source").column == "source" + assert Relation._meta.get_field("type").column == "type" + assert Relation._meta.get_field("target").column == "target" + + +def test_relation_foreign_keys_reference_expected_models(): + source = Relation._meta.get_field("source") + relation_type = Relation._meta.get_field("type") + target = Relation._meta.get_field("target") + + assert source.remote_field.model is Concept + assert relation_type.remote_field.model is RelationType + assert target.remote_field.model is Concept + + +def test_relation_reverse_names_are_explicit(): + source = Relation._meta.get_field("source") + relation_type = Relation._meta.get_field("type") + target = Relation._meta.get_field("target") + + assert source.remote_field.related_name == "outgoing_relations" + assert relation_type.remote_field.related_name == "relations" + assert target.remote_field.related_name == "incoming_relations" + + +def test_relation_deletion_behaviour_matches_database_schema(): + source = Relation._meta.get_field("source") + relation_type = Relation._meta.get_field("type") + target = Relation._meta.get_field("target") + + assert source.remote_field.on_delete is CASCADE + assert relation_type.remote_field.on_delete is RESTRICT + assert target.remote_field.on_delete is CASCADE + + +def test_relation_declares_semantic_unique_constraint(): + constraint = next( + constraint + for constraint in Relation._meta.constraints + if constraint.name == "unique_typed_relation" + ) + + assert tuple(constraint.fields) == ("source", "type", "target") + + +def test_relation_string_representation_uses_semantic_identity(): + relation = Relation( + id=uuid.uuid4(), + source_id="matrix", + type_id="subtype_of", + target_id="data-structure", + ) + + assert str(relation) == "matrix —subtype_of→ data-structure"