TRA-236 — Course domain model: - courses/models.py: Course → Module → Lesson → Page hierarchy with UUID PKs, ordering fields, required_seconds navigation gate, version tracking, and UniqueConstraint per (parent, order) pair - courses/migrations/0001_initial.py: initial migration (applies cleanly on a fresh DB) - tests/test_course_domain.py: migration smoke, relation integrity, cascade delete, ordering, and uniqueness-constraint tests TRA-238 — Dwell-time tracking: - tracking/models.py: Enrollment, PageProgress (can_advance property), and DwellEvent models appended alongside existing AuditEvent - tracking/services.py: record_dwell_event, compute_eligible_seconds (pure), check_navigation_gate, _recompute_accumulated — reconnect merging within RECONNECT_TOLERANCE_SECONDS and anti-idle cap at MAX_VALID_EVENT_SECONDS - tracking/migrations/0001_initial.py: updated to include all four models (AuditEvent, Enrollment, PageProgress, DwellEvent) with FK dependencies on courses.Course and courses.Page - tests/test_dwell_tracking.py: event replay, reconnect tolerance, anti-idle cap, gate pass/block, and can_advance DB integration tests Co-Authored-By: Paperclip <noreply@paperclip.ing>
97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
import uuid
|
|
|
|
from django.db import models
|
|
|
|
|
|
class Course(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
org_id = models.UUIDField(db_index=True)
|
|
title = models.CharField(max_length=255)
|
|
slug = models.SlugField(max_length=255, unique=True)
|
|
description = models.TextField(blank=True)
|
|
version = models.PositiveIntegerField(default=1)
|
|
is_published = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ["title"]
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
|
|
class Module(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="modules")
|
|
title = models.CharField(max_length=255)
|
|
order = models.PositiveIntegerField(default=0)
|
|
version = models.PositiveIntegerField(default=1)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ["order"]
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["course", "order"], name="unique_course_module_order")
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.course.title} / {self.title}"
|
|
|
|
|
|
class Lesson(models.Model):
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name="lessons")
|
|
title = models.CharField(max_length=255)
|
|
order = models.PositiveIntegerField(default=0)
|
|
version = models.PositiveIntegerField(default=1)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ["order"]
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["module", "order"], name="unique_module_lesson_order")
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.module} / {self.title}"
|
|
|
|
|
|
class Page(models.Model):
|
|
CONTENT_TEXT = "text"
|
|
CONTENT_VIDEO = "video"
|
|
CONTENT_QUIZ = "quiz"
|
|
CONTENT_EMBED = "embed"
|
|
CONTENT_TYPE_CHOICES = [
|
|
(CONTENT_TEXT, "Text"),
|
|
(CONTENT_VIDEO, "Video"),
|
|
(CONTENT_QUIZ, "Quiz"),
|
|
(CONTENT_EMBED, "Embed"),
|
|
]
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name="pages")
|
|
title = models.CharField(max_length=255)
|
|
order = models.PositiveIntegerField(default=0)
|
|
content_type = models.CharField(
|
|
max_length=20, choices=CONTENT_TYPE_CHOICES, default=CONTENT_TEXT
|
|
)
|
|
content_body = models.TextField(blank=True)
|
|
is_required = models.BooleanField(default=True)
|
|
# Minimum active dwell time (seconds) a learner must accumulate before advancing.
|
|
required_seconds = models.PositiveIntegerField(default=0)
|
|
version = models.PositiveIntegerField(default=1)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ["order"]
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["lesson", "order"], name="unique_lesson_page_order")
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.lesson} / {self.title}"
|