Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion core/models.py
Original file line number Diff line number Diff line change
@@ -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}"
77 changes: 77 additions & 0 deletions tests/test_curriculum_models.py
Original file line number Diff line number Diff line change
@@ -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"