import datetime import re from typing import TYPE_CHECKING, TypedDict from django.conf import settings from django.core.validators import RegexValidator from django.db import models from django.db.models import OuterRef, Q, Subquery from django_stubs_ext import WithAnnotations from semver import VersionInfo from membershipworks.models import Flag as MembershipWorksFlag from membershipworks.models import Member class AbstractAudit(models.Model): date = models.DateField(default=datetime.date.today, unique=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) good = models.BooleanField(default=False) notes = models.CharField(max_length=255, blank=True) class Meta: abstract = True ordering = ["date"] get_latest_by = ["date"] def __str__(self) -> str: return f"{'Good' if self.good else 'Bad'} audit at {self.date} by {self.user}" class CmsRedRiverVeteransScholarship(models.Model): serial = models.AutoField(primary_key=True) program_name = models.CharField(db_column="Program Name", max_length=255) member_name = models.CharField( db_column="Member Name", max_length=255, blank=True, null=True ) discount_percent = models.DecimalField( db_column="Discount Percent", max_digits=16, decimal_places=0, blank=True, null=True, ) discount_code = models.CharField( db_column="Discount Code", max_length=255, blank=True, null=True ) membership_code = models.CharField( db_column="Membership Code", max_length=255, blank=True, null=True ) start_date = models.DateField(db_column="Start Date", blank=True, null=True) end_date = models.DateField(db_column="End Date", blank=True, null=True) program_amount = models.DecimalField( db_column="Program Amount", max_digits=16, decimal_places=0, blank=True, null=True, ) program_status = models.CharField( db_column="Program Status", max_length=16, blank=True, null=True ) class Meta: db_table = "CMS Red River Veterans Scholarship" def __str__(self) -> str: return f"{self.program_name} {self.member_name}" class DepartmentQuerySet(models.QuerySet): def filter_by_shop_lead(self, member: Member) -> models.QuerySet["Department"]: """Get departments for which `member` is a shop lead""" # TODO: should select children recursively, instead of specific levels return self.filter( Q(shop_lead_flag__members=member) | Q(parent__shop_lead_flag__members=member) | Q(parent__parent__shop_lead_flag__members=member) ) class Department(models.Model): name = models.CharField( max_length=64, validators=[RegexValidator("^[-_ A-Za-z0-9]*$")], help_text="This will also be used to generate the mailing list name", ) parent = models.ForeignKey( "self", on_delete=models.PROTECT, related_name="children", blank=True, null=True ) has_mailing_list = models.BooleanField(default=False) shop_lead_flag = models.ForeignKey( MembershipWorksFlag, on_delete=models.PROTECT, blank=True, null=True, db_constraint=False, help_text="This will also be used to set the moderators for the mailing list", ) list_reply_to_address = models.EmailField(max_length=254, blank=True) objects = DepartmentQuerySet.as_manager() def __str__(self) -> str: return self.name @property def list_name(self) -> str | None: if self.has_mailing_list: return self.name.replace(" ", "_") + "-info" else: return None @property def list_address(self) -> str | None: if self.list_name: return self.list_name + "@claremontmakerspace.org" else: return None class CertificationDefinition(models.Model): name = models.CharField(max_length=255) department = models.ForeignKey(Department, models.PROTECT) class Meta: db_table = "Certification Definitions" ordering = ("name", "department") def __str__(self) -> str: return f"{self.name} <{self.department}>" def latest_version(self) -> "CertificationVersion": return self.certificationversion_set.latest() class CertificationVersionAnnotations(TypedDict): is_latest: bool is_current: bool class CertificationVersionManager(models.Manager["CertificationVersion"]): def get_queryset(self) -> models.QuerySet["CertificationVersion"]: qs = super().get_queryset() department_versions = qs.filter(definition=OuterRef("definition")).order_by( "-major", "-minor", "-patch", "-prerelease", ) return qs.annotate( is_latest=Q(pk=Subquery(department_versions.values("pk")[:1])), # TODO: should do a more correct comparison than just major version is_current=Q(major=Subquery(department_versions.values("major")[:1])), ) class CertificationVersion(models.Model): definition = models.ForeignKey(CertificationDefinition, on_delete=models.PROTECT) major = models.PositiveSmallIntegerField() minor = models.PositiveSmallIntegerField() patch = models.PositiveSmallIntegerField() prerelease = models.CharField(max_length=255, blank=True) approval_date = models.DateField(blank=True, null=True) objects = CertificationVersionManager() class Meta: constraints = [ models.UniqueConstraint( fields=["definition", "major", "minor", "patch", "prerelease"], name="unique_certification_version", ) ] ordering = ( "definition", "major", "minor", "patch", "prerelease", "approval_date", ) get_latest_by = ("major", "minor", "patch", "prerelease", "approval_date") base_manager_name = "objects" def __str__(self) -> str: return f"{self.definition} [{self.semantic_version()}]" def semantic_version(self) -> VersionInfo: return VersionInfo( self.major or 0, self.minor or 0, self.patch or 0, re.sub(r"[^.a-zA-Z0-9]", "-", self.prerelease), self.approval_date.isoformat() if self.approval_date is not None else None, ) if TYPE_CHECKING: CertificationVersionAnnotated = WithAnnotations[ CertificationVersion, CertificationVersionAnnotations ] else: CertificationVersionAnnotated = WithAnnotations[CertificationVersion] class Certification(models.Model): number = models.AutoField(primary_key=True) certification_version = models.ForeignKey( CertificationVersion, on_delete=models.PROTECT ) name = models.CharField(max_length=255) member = models.ForeignKey( Member, on_delete=models.PROTECT, to_field="uid", blank=True, null=True, db_constraint=False, ) certified_by = models.CharField(max_length=255, blank=True, null=True) date = models.DateField(blank=True, null=True) shop_lead_notified = models.DateTimeField(blank=True, null=True) notes = models.CharField(max_length=255, blank=True, null=True) class Meta: db_table = "Certifications" permissions = [ ( "receive_certification_emails", "Receives notifications of all new certifications", ), ] def __str__(self) -> str: return f"{self.name} - {self.certification_version}" class CertificationAudit(AbstractAudit): certification = models.ForeignKey( Certification, on_delete=models.CASCADE, related_name="audits" ) class InstructorOrVendor(models.Model): serial = models.AutoField(primary_key=True) name = models.CharField(db_column="Name", max_length=255) instructor_agreement_date = models.DateField( db_column="Instructor Agreement Date", blank=True, null=True ) w9_date = models.DateField(db_column="W9 date", blank=True, null=True) phone = models.CharField(max_length=255, blank=True, null=True) email_address = models.CharField( db_column="email address", max_length=255, blank=True, null=True ) class Meta: db_table = "Instructors and Vendors" def __str__(self) -> str: return f"{self.name}" class SpecialProgram(models.Model): program_name = models.CharField( db_column="Program Name", primary_key=True, max_length=255 ) discount_percent = models.DecimalField( db_column="Discount Percent", max_digits=16, decimal_places=0, blank=True, null=True, ) discount_code = models.CharField( db_column="Discount Code", max_length=255, blank=True, null=True ) membership_code = models.CharField( db_column="Membership Code", max_length=255, blank=True, null=True ) start_date = models.DateField(db_column="Start Date", blank=True, null=True) end_date = models.DateField(db_column="End Date", blank=True, null=True) program_amount = models.DecimalField( db_column="Program Amount", max_digits=16, decimal_places=0, blank=True, null=True, ) program_status = models.CharField( db_column="Program Status", max_length=16, blank=True, null=True ) class Meta: db_table = "Special_Programs" def __str__(self) -> str: return self.program_name class Waiver(models.Model): number = models.AutoField(db_column="Number", primary_key=True) name = models.CharField(db_column="Name", max_length=255) date = models.DateField(db_column="Date") emergency_contact_name = models.CharField( db_column="Emergency Contact Name", max_length=255, blank=True, null=True ) emergency_contact_number = models.CharField( db_column="Emergency Contact Number", max_length=25, blank=True, null=True ) waiver_version = models.CharField(db_column="Waiver version", max_length=64) guardian_name = models.CharField( db_column="Guardian Name", max_length=255, blank=True, null=True ) guardian_relation = models.CharField( db_column="Guardian Relation", max_length=255, blank=True, null=True ) guardian_date = models.DateField(db_column="Guardian Date", blank=True, null=True) notes = models.CharField(max_length=255, blank=True, null=True) class Meta: db_table = "Waivers" def __str__(self) -> str: return f"{self.name} {self.date}" class WaiverAudit(AbstractAudit): waiver = models.ForeignKey(Waiver, on_delete=models.CASCADE, related_name="audits")