cmsmanage/paperwork/models.py

331 lines
11 KiB
Python

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)
def __str__(self) -> str:
return f"{'Good' if self.good else 'Bad'} audit at {self.date} by {self.user}"
class Meta:
abstract = True
ordering = ["date"]
get_latest_by = ["date"]
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
)
def __str__(self) -> str:
return f"{self.program_name} {self.member_name}"
class Meta:
db_table = "CMS Red River Veterans Scholarship"
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):
objects = DepartmentQuerySet.as_manager()
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)
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)
def __str__(self) -> str:
return f"{self.name} <{self.department}>"
class Meta:
db_table = "Certification Definitions"
ordering = ("name", "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):
objects = CertificationVersionManager()
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)
def __str__(self) -> str:
return f"{self.definition} [{self.semantic_version()}]"
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 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)
def __str__(self) -> str:
return f"{self.name} - {self.certification_version}"
class Meta:
db_table = "Certifications"
permissions = [
(
"receive_certification_emails",
"Receives notifications of all new certifications",
),
]
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
)
def __str__(self) -> str:
return f"{self.name}"
class Meta:
db_table = "Instructors and Vendors"
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
)
def __str__(self) -> str:
return self.program_name
class Meta:
db_table = "Special_Programs"
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)
def __str__(self) -> str:
return f"{self.name} {self.date}"
class Meta:
db_table = "Waivers"
class WaiverAudit(AbstractAudit):
waiver = models.ForeignKey(Waiver, on_delete=models.CASCADE, related_name="audits")