Files
training-software/courses/models.py
Paperclip CTO cfa9ad6f53 feat(TRA-236,TRA-238): M2 course domain model and dwell-time tracking
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>
2026-05-07 09:11:23 +02:00

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}"