- TrainingRecord model with completion_mode (online/offline/blended) and CompletionStatus state machine - TrainerSignoff model with decision, notes, trainer FK, and audit timestamp - SignoffEvidence model for uploaded proof files - services.py state transitions: - mark_in_progress: not_started → in_progress - mark_online_passed: online → completed; blended → pending_signoff - submit_trainer_signoff: offline/blended approved → completed; rejected → in_progress - InvalidTransitionError on illegal state moves - IsTrainer permission class based on training:signoff capability - API: record detail, start, mark-online-passed, trainer-signoff, pending-signoff list - 20 unit + integration tests covering all mode paths, invalid transitions, and access control - Blended completion requires both online pass AND trainer approval Co-Authored-By: Paperclip <noreply@paperclip.ing>
95 lines
3.9 KiB
Python
95 lines
3.9 KiB
Python
import uuid
|
|
|
|
import django.db.models.deletion
|
|
from django.conf import settings
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
initial = True
|
|
|
|
dependencies = [
|
|
("accounts", "0001_initial"),
|
|
("tracking", "0001_initial"),
|
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
]
|
|
|
|
operations = [
|
|
migrations.CreateModel(
|
|
name="TrainingRecord",
|
|
fields=[
|
|
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
("completion_mode", models.CharField(
|
|
choices=[("online", "Online"), ("offline", "Offline"), ("blended", "Blended")],
|
|
default="online",
|
|
max_length=20,
|
|
)),
|
|
("status", models.CharField(
|
|
choices=[
|
|
("not_started", "Not Started"),
|
|
("in_progress", "In Progress"),
|
|
("pending_signoff", "Pending Trainer Signoff"),
|
|
("completed", "Completed"),
|
|
("failed", "Failed"),
|
|
],
|
|
default="not_started",
|
|
max_length=20,
|
|
)),
|
|
("online_passed_at", models.DateTimeField(blank=True, null=True)),
|
|
("completed_at", models.DateTimeField(blank=True, null=True)),
|
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
("updated_at", models.DateTimeField(auto_now=True)),
|
|
("enrollment", models.OneToOneField(
|
|
on_delete=django.db.models.deletion.CASCADE,
|
|
related_name="training_record",
|
|
to="tracking.enrollment",
|
|
)),
|
|
],
|
|
options={"db_table": "training_record"},
|
|
),
|
|
migrations.CreateModel(
|
|
name="TrainerSignoff",
|
|
fields=[
|
|
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
("decision", models.CharField(
|
|
choices=[("approved", "Approved"), ("rejected", "Rejected")],
|
|
max_length=20,
|
|
)),
|
|
("notes", models.TextField(blank=True)),
|
|
("signed_off_at", models.DateTimeField(auto_now_add=True)),
|
|
("training_record", models.ForeignKey(
|
|
on_delete=django.db.models.deletion.CASCADE,
|
|
related_name="signoffs",
|
|
to="training.trainingrecord",
|
|
)),
|
|
("trainer", models.ForeignKey(
|
|
on_delete=django.db.models.deletion.CASCADE,
|
|
related_name="given_signoffs",
|
|
to=settings.AUTH_USER_MODEL,
|
|
)),
|
|
],
|
|
options={"db_table": "training_trainer_signoff", "ordering": ["-signed_off_at"]},
|
|
),
|
|
migrations.CreateModel(
|
|
name="SignoffEvidence",
|
|
fields=[
|
|
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
("file_name", models.CharField(max_length=255)),
|
|
("file_path", models.CharField(max_length=1000)),
|
|
("uploaded_at", models.DateTimeField(auto_now_add=True)),
|
|
("training_record", models.ForeignKey(
|
|
on_delete=django.db.models.deletion.CASCADE,
|
|
related_name="evidence",
|
|
to="training.trainingrecord",
|
|
)),
|
|
("uploaded_by", models.ForeignKey(
|
|
on_delete=django.db.models.deletion.CASCADE,
|
|
related_name="uploaded_evidence",
|
|
to=settings.AUTH_USER_MODEL,
|
|
)),
|
|
],
|
|
options={"db_table": "training_signoff_evidence"},
|
|
),
|
|
]
|