from django.db.models import ( CASCADE, CharField, FileField, ForeignKey, UUIDField, Model, TextField, ManyToManyField, DateTimeField, BooleanField, TextChoices, ) from django.utils.translation import gettext_lazy as _ from uuid import uuid4 from datetime import timedelta from django.utils import timezone from apps.users.models import TimeStampMixin, User class Organization(TimeStampMixin, Model): name = CharField(max_length=255, unique=True) uuid = UUIDField(default=uuid4, editable=False, unique=True) description = TextField(blank=True, default="") owner = ForeignKey(User, on_delete=CASCADE, related_name="owned_organizations") members = ManyToManyField(User, through="OrganizationMembership", related_name="organizations") class Meta: verbose_name = _("Organization") verbose_name_plural = _("Organizations") def __str__(self) -> str: return self.name class OrganizationMembership(TimeStampMixin, Model): class Role(TextChoices): EMPLOYER = "employer", _("Employer") EMPLOYEE = "employee", _("Employee") user = ForeignKey(User, on_delete=CASCADE, related_name="organization_memberships") organization = ForeignKey(Organization, on_delete=CASCADE, related_name="memberships") role = CharField(max_length=50, choices=Role.choices, default=Role.EMPLOYEE) class Meta: verbose_name = _("Organization Membership") verbose_name_plural = _("Organization Memberships") unique_together = [["user", "organization"]] def __str__(self) -> str: return f"{self.user.full_name} - {self.organization.name} ({self.role})" class InviteToken(TimeStampMixin, Model): token = UUIDField(default=uuid4, unique=True, editable=False) organization = ForeignKey(Organization, on_delete=CASCADE, related_name="invite_tokens") created_by = ForeignKey(User, on_delete=CASCADE, related_name="created_invites") expires_at = DateTimeField() used_by = ForeignKey(User, on_delete=CASCADE, null=True, blank=True, related_name="used_invites") used_at = DateTimeField(null=True, blank=True) is_active = BooleanField(default=True) class Meta: verbose_name = _("Invite Token") verbose_name_plural = _("Invite Tokens") 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 not self.used_by and timezone.now() < self.expires_at def __str__(self) -> str: return f"Invite for {self.organization.name} (expires {self.expires_at})" class Domain(Model): name = CharField(max_length=255, unique=True) uuid = UUIDField(default=uuid4, editable=False, unique=True) description = TextField(blank=True, default="") organization = ForeignKey(Organization, on_delete=CASCADE, related_name="domains", null=True, blank=True) members = ManyToManyField(User, through="DomainMembership", related_name="domains") class Meta: verbose_name = _("Domain") verbose_name_plural = _("Domains") def __str__(self) -> str: return self.name class DomainMembership(TimeStampMixin, Model): user = ForeignKey(User, on_delete=CASCADE, related_name="domain_memberships") domain = ForeignKey(Domain, on_delete=CASCADE, related_name="memberships") class Meta: verbose_name = _("Domain Membership") verbose_name_plural = _("Domain Memberships") unique_together = [["user", "domain"]] def __str__(self) -> str: return f"{self.user.full_name} - {self.domain.name}" class Dataset(TimeStampMixin, Model): domain = ForeignKey(Domain, on_delete = CASCADE, related_name = "datasets") name = CharField(max_length = 255) uuid = UUIDField(default = uuid4, editable = False, unique = True) description = TextField(blank = True, default = "") created_by = ForeignKey(User, on_delete = CASCADE, related_name = "created_datasets") datafile = FileField(upload_to = "datasets/") def __str__(self) -> str: return f"{self.name} ({self.domain.name})" Organisation = Organization