Dynavera/apps/accounts/models.py

189 lines
8.5 KiB
Python
Raw Normal View History

from datetime import timedelta
2026-03-08 13:10:49 +00:00
from typing import ClassVar
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.core.exceptions import ValidationError
from django.db import transaction
2026-03-08 13:10:49 +00:00
from django.db.models import CASCADE, BooleanField, CharField, DateField, DateTimeField, EmailField, ForeignKey, IntegerField, ManyToManyField, Model, TextField
from django.db.models.signals import m2m_changed, post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone
2026-03-08 13:10:49 +00:00
from django.utils.translation import gettext_lazy as _
from apps.accounts.managers import UserManager
from apps.accounts.mixins import IdentifierMixin, TimeStampMixin
class User(AbstractBaseUser, IdentifierMixin, TimeStampMixin, PermissionsMixin):
email_address = EmailField(verbose_name = _("Email Address"), max_length = 255, unique = True)
first_name = CharField(verbose_name = _("First Name"), max_length = 255)
last_name = CharField(verbose_name = _("Last Name"), max_length = 255)
date_of_birth = DateField(verbose_name = _("Date of Birth"), null = True, blank = True)
is_active = BooleanField(verbose_name = _("Account Active"), default = True)
is_staff = BooleanField(verbose_name = _("Account Admin"), default = False)
is_manager = BooleanField(verbose_name = _("Organization Manager"), default = False)
USERNAME_FIELD = 'email_address'
EMAIL_FIELD = 'email_address'
REQUIRED_FIELDS = ['first_name', 'last_name', 'date_of_birth']
objects: ClassVar[UserManager] = UserManager()
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
class Meta:
verbose_name = _('User')
verbose_name_plural = _('Users')
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
def is_owner_of(self, organization: 'Organization') -> bool:
return organization.owner.id == self.id
def is_member_of(self, organization: 'Organization') -> bool:
return organization.members.filter(id=self.id).exists()
def __str__(self) -> str:
return self.full_name
class Organization(IdentifierMixin, TimeStampMixin, Model):
name = CharField(verbose_name = _("Name"), max_length = 255, unique = True)
description = TextField(verbose_name = _("Description"), blank = True, default = '')
owner = ForeignKey(User, on_delete = CASCADE, related_name = 'owned_organizations')
members = ManyToManyField(User, related_name = 'organizations')
class Meta:
verbose_name = _('Organization')
verbose_name_plural = _('Organizations')
def __str__(self) -> str:
return self.name
class Invite(IdentifierMixin, TimeStampMixin, Model):
organization = ForeignKey(Organization, on_delete = CASCADE, related_name = "invites")
created_by = ForeignKey(User, on_delete = CASCADE, related_name = "created_invites")
expires_at = DateTimeField(verbose_name=_("Expires At"))
uses = IntegerField(verbose_name=_("Uses"), default = 0)
max_uses = IntegerField(verbose_name=_("Max Uses"), default = 1)
is_active = BooleanField(verbose_name=_("Is Active"), default = True)
class Meta:
verbose_name = _("Invite")
verbose_name_plural = _("Invites")
def save(self, *args, **kwargs):
if not self.expires_at:
self.expires_at = timezone.now() + timedelta(days=7)
super().save(*args, **kwargs)
def is_valid(self):
return self.is_active and self.uses < self.max_uses and timezone.now() < self.expires_at
def __str__(self) -> str:
return f"Invite for {self.organization.name} by {self.created_by.full_name} (expires {self.expires_at})"
class Role(IdentifierMixin, TimeStampMixin, Model):
name = CharField(verbose_name = _("Name"), max_length = 100)
description = TextField(verbose_name = _("Description"), blank = True, default = '')
organization = ForeignKey(Organization, on_delete = CASCADE, related_name = "roles")
members = ManyToManyField(User, related_name = "roles")
class Meta:
verbose_name = _('Role')
verbose_name_plural = _('Roles')
unique_together = [('organization', 'name')]
def __str__(self) -> str:
return f"{self.name} ({self.organization.name})"
@receiver(post_save, sender=Role)
def create_default_agents_for_role(sender, instance: Role, created: bool, **kwargs):
if created:
2026-03-08 13:10:49 +00:00
from apps.onboarding.models import AgentConfig # L: circular import :(
default_agents = [
{
'type': 'curriculum',
'name': f"{instance.name} Curriculum Agent",
'prompt': (
f"You are an instructional design assistant for onboarding the role '{instance.name}'. "
"Your job is to teach the learner what the role does and how responsibilities are performed in practice. "
"Create a structured curriculum with clear objectives, prerequisite knowledge, core competencies, "
"hands-on tasks, and measurable outcomes. Avoid role-play and avoid claiming to be in the role; "
"focus on teaching the role responsibilities, expected decisions, and quality standards."
)
},
{
'type': 'knowledge',
'name': f"{instance.name} Knowledge Agent",
'prompt': (
f"You are a domain knowledge tutor for the role '{instance.name}'. "
"Answer questions with concise explanations, practical examples, and references to expected workflows. "
"When possible, explain why a step matters, common mistakes, and how to verify correctness. "
"Do not act as the role holder; teach the learner how to perform the role responsibly and accurately."
)
},
{
'type': 'assessment',
'name': f"{instance.name} Assessment Agent",
'prompt': (
f"You are an assessment designer for onboarding the role '{instance.name}'. "
"Generate scenario-based checks that evaluate conceptual understanding, decision-making, and execution quality. "
"Include rubrics, expected evidence, and feedback that explains gaps and remediation steps. "
"Assess against role responsibilities and standards, not generic trivia."
)
},
{
'type': 'monitor',
'name': f"{instance.name} Progress Monitor",
'prompt': (
f"You are a progress coaching assistant for learners training for the role '{instance.name}'. "
"Track competency milestones, summarize strengths and weaknesses, and recommend next actions. "
"Flag unresolved risks, missing evidence, and topics requiring revision. "
"Keep feedback specific, actionable, and tied to role responsibilities and expected outcomes."
)
}
]
with transaction.atomic():
for agent_data in default_agents:
AgentConfig.objects.create(
organization=instance.organization,
role=instance,
name=agent_data['name'],
agent_type=agent_data['type'],
system_prompt=agent_data['prompt'],
llm_config={"model_id": "meta-llama-3.1-8b-instruct"}
)
@receiver(post_delete, sender=Role)
def delete_role_agents_on_role_delete(sender, instance: Role, **kwargs):
from apps.onboarding.models import AgentConfig
AgentConfig.objects.filter(role=instance).delete()
@receiver(post_save, sender=Organization)
def ensure_owner_is_member(sender, instance: Organization, **kwargs):
if instance.owner.id and not instance.members.filter(id=instance.owner.id).exists():
instance.members.add(instance.owner)
@receiver(m2m_changed, sender=Organization.members.through)
def prevent_owner_from_being_removed(sender, instance: Organization, action: str, pk_set, **kwargs):
if action == 'pre_remove' and instance.owner.id in (pk_set or set()):
raise ValidationError(_('Organization owner must remain a member.'))
if action in ['post_add', 'post_remove', 'post_clear']:
if instance.owner.id and not instance.members.filter(id=instance.owner.id).exists():
instance.members.add(instance.owner)