from typing import Optional from datetime import datetime import django.core.mail.message from django.conf import settings from django.db import models from django.db.models import Exists, OuterRef from django.utils import timezone class BaseModel(models.Model): _csv_headers_override = {} _date_fields = {} class Meta: abstract = True @classmethod def _remap_headers(cls, data): for field in cls._meta.get_fields(): # TODO: more robust filtering of fields that don't have a column if field.auto_created or field.many_to_many or not field.concrete: continue field_name, csv_header = field.get_attname_column() if field_name in cls._csv_headers_override: csv_header = cls._csv_headers_override[field.name] yield field_name, data[csv_header] @classmethod def from_csv_dict(cls, data): data = data.copy() # parse date fields to datetime objects for field, fmt in cls._date_fields.items(): if data[field]: data[field] = datetime.strptime(str(data[field]), fmt) else: # convert empty string to None to make NULL in SQL data[field] = None return cls(**dict(cls._remap_headers(data))) class Flag(BaseModel): id = models.CharField(max_length=24, primary_key=True) name = models.TextField(null=True, blank=True) type = models.CharField(max_length=6) def __str__(self): return f"{self.name} ({self.type})" class Meta: db_table = "flag" ordering = ("name",) class MemberQuerySet(models.QuerySet): # TODO: maybe rename to reflect EXISTS? @staticmethod def has_flag(flag_type: str, flag_name: str): return Exists( Flag.objects.filter(type=flag_type, name=flag_name, members=OuterRef("pk")) ) # TODO: it should be fairly easy to reduce the number of EXISTS by # merging the ORed flags def with_is_active(self): return self.annotate( is_active=( self.has_flag("folder", "Members") | self.has_flag("folder", "CMS Staff") ) & ~( self.has_flag("label", "Account On Hold") | self.has_flag("level", "CMS Membership on hold") | self.has_flag("folder", "Former Members") ) ) # TODO: is this still a temporal table? class Member(BaseModel): objects = MemberQuerySet.as_manager() uid = models.CharField(max_length=24, primary_key=True) year_of_birth = models.TextField(db_column="Year of Birth", null=True, blank=True) account_name = models.TextField(db_column="Account Name", null=True, blank=True) first_name = models.TextField(db_column="First Name", null=True, blank=True) last_name = models.TextField(db_column="Last Name", null=True, blank=True) phone = models.TextField(db_column="Phone", null=True, blank=True) email = models.TextField(db_column="Email", null=True, blank=True) volunteer_email = models.TextField( db_column="Volunteer Email", null=True, blank=True ) address_street = models.TextField( db_column="Address (Street)", null=True, blank=True ) address_city = models.TextField(db_column="Address (City)", null=True, blank=True) address_state_province = models.TextField( db_column="Address (State/Province)", null=True, blank=True ) address_postal_code = models.TextField( db_column="Address (Postal Code)", null=True, blank=True ) address_country = models.TextField( db_column="Address (Country)", null=True, blank=True ) profile_description = models.TextField( db_column="Profile description", null=True, blank=True ) website = models.TextField(db_column="Website", null=True, blank=True) fax = models.TextField(db_column="Fax", null=True, blank=True) contact_person = models.TextField(db_column="Contact Person", null=True, blank=True) password = models.TextField(db_column="Password", null=True, blank=True) position_relation = models.TextField( db_column="Position/relation", null=True, blank=True ) parent_account_id = models.TextField( db_column="Parent Account ID", null=True, blank=True ) closet_storage = models.TextField( db_column="Closet Storage #", null=True, blank=True ) storage_shelf = models.TextField(db_column="Storage Shelf #", null=True, blank=True) personal_studio_space = models.TextField( db_column="Personal Studio Space #", null=True, blank=True ) access_permitted_shops_during_extended_hours = models.BooleanField( db_column="Access Permitted Shops During Extended Hours?" ) access_front_door_and_studio_space_during_extended_hours = models.BooleanField( db_column="Access Front Door and Studio Space During Extended Hours?" ) access_wood_shop = models.BooleanField(db_column="Access Wood Shop?") access_metal_shop = models.BooleanField(db_column="Access Metal Shop?") access_storage_closet = models.BooleanField(db_column="Access Storage Closet?") access_studio_space = models.BooleanField(db_column="Access Studio Space?") access_front_door = models.BooleanField(db_column="Access Front Door?") access_card_number = models.TextField( db_column="Access Card Number", null=True, blank=True ) access_card_facility_code = models.TextField( db_column="Access Card Facility Code", null=True, blank=True ) auto_billing_id = models.TextField( db_column="Auto Billing ID", null=True, blank=True ) billing_method = models.TextField(db_column="Billing Method", null=True, blank=True) renewal_date = models.DateField(db_column="Renewal Date", null=True, blank=True) join_date = models.DateField(db_column="Join Date", null=True, blank=True) admin_note = models.TextField(db_column="Admin note", null=True, blank=True) profile_gallery_image_url = models.TextField( db_column="Profile gallery image URL", null=True, blank=True ) business_card_image_url = models.TextField( db_column="Business card image URL", null=True, blank=True ) instagram = models.TextField(db_column="Instagram", null=True, blank=True) pinterest = models.TextField(db_column="Pinterest", null=True, blank=True) youtube = models.TextField(db_column="Youtube", null=True, blank=True) yelp = models.TextField(db_column="Yelp", null=True, blank=True) google = models.TextField(db_column="Google+", null=True, blank=True) bbb = models.TextField(db_column="BBB", null=True, blank=True) twitter = models.TextField(db_column="Twitter", null=True, blank=True) facebook = models.TextField(db_column="Facebook", null=True, blank=True) linked_in = models.TextField(db_column="LinkedIn", null=True, blank=True) do_not_show_street_address_in_profile = models.TextField( db_column="Do not show street address in profile", null=True, blank=True ) do_not_list_in_directory = models.TextField( db_column="Do not list in directory", null=True, blank=True ) how_did_you_hear = models.TextField( db_column="HowDidYouHear", null=True, blank=True ) authorize_charge = models.TextField( db_column="authorizeCharge", null=True, blank=True ) policy_agreement = models.TextField( db_column="policyAgreement", null=True, blank=True ) waiver_form_signed_and_on_file_date = models.DateField( db_column="Waiver form signed and on file date.", null=True, blank=True ) membership_agreement_signed_and_on_file_date = models.DateField( db_column="Membership Agreement signed and on file date.", null=True, blank=True ) ip_address = models.TextField(db_column="IP Address", null=True, blank=True) audit_date = models.DateField(db_column="Audit Date", null=True, blank=True) agreement_version = models.TextField( db_column="Agreement Version", null=True, blank=True ) paperwork_status = models.TextField( db_column="Paperwork status", null=True, blank=True ) membership_agreement_dated = models.BooleanField( db_column="Membership agreement dated" ) membership_agreement_acknowledgement_page_filled_out = models.BooleanField( db_column="Membership Agreement Acknowledgement Page Filled Out" ) membership_agreement_signed = models.BooleanField( db_column="Membership Agreement Signed" ) liability_form_filled_out = models.BooleanField( db_column="Liability Form Filled Out" ) flags = models.ManyToManyField(Flag, through="MemberFlag", related_name="members") _csv_headers_override = { "uid": "Account ID", "how_did_you_hear": "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:", "authorize_charge": "Yes - I authorize TwinState MakerSpaces, Inc. to charge my credit card for the membership and other options that I have selected.", "policy_agreement": "I have read the Claremont MakerSpace Membership Agreement & Policies, and agree to all terms stated therein.", } _date_fields = { "Join Date": "%b %d, %Y", "Renewal Date": "%b %d, %Y", "Audit Date": "%m/%d/%Y", "Membership Agreement signed and on file date.": "%m/%d/%Y", "Waiver form signed and on file date.": "%m/%d/%Y", } def __str__(self): return f"{self.account_name}" class Meta: db_table = "members" ordering = ("first_name", "last_name") indexes = [ models.Index(fields=["account_name"], name="account_name_idx"), models.Index(fields=["first_name"], name="first_name_idx"), models.Index(fields=["last_name"], name="last_name_idx"), ] @classmethod def from_user(cls, user) -> Optional["Member"]: if hasattr(user, "ldap_user"): return cls.objects.get(uid=user.ldap_user.attrs["employeeNumber"][0]) def sanitized_mailbox(self, name_ext: str = "", use_volunteer=False) -> str: if use_volunteer and self.volunteer_email: email = self.volunteer_email elif self.email: email = self.email else: raise Exception(f"No Email Address for user: {self.uid}") if not self.account_name: return email return django.core.mail.message.sanitize_address( (self.account_name + name_ext, email), settings.DEFAULT_CHARSET ) class MemberFlag(BaseModel): member = models.ForeignKey( Member, on_delete=models.PROTECT, db_column="uid", db_constraint=False ) flag = models.ForeignKey(Flag, on_delete=models.PROTECT) def __str__(self): return f"{self.member} - {self.flag}" class Meta: db_table = "memberflag" constraints = [ models.UniqueConstraint( fields=["member", "flag"], name="unique_member_flag" ) ] class Transaction(BaseModel): sid = models.CharField(max_length=256, null=True, blank=True) member = models.ForeignKey( Member, on_delete=models.PROTECT, db_column="uid", db_constraint=False, related_name="transactions", null=True, blank=True, ) timestamp = models.DateTimeField() type = models.TextField(null=True, blank=True) sum = models.DecimalField(max_digits=13, decimal_places=4, null=True, blank=True) fee = models.DecimalField(max_digits=13, decimal_places=4, null=True, blank=True) event_id = models.TextField(null=True, blank=True) for_what = models.TextField(db_column="For", null=True, blank=True) items = models.TextField(db_column="Items", null=True, blank=True) discount_code = models.TextField(db_column="Discount Code", null=True, blank=True) note = models.TextField(db_column="Note", null=True, blank=True) name = models.TextField(db_column="Name", null=True, blank=True) contact_person = models.TextField(db_column="Contact Person", null=True, blank=True) full_address = models.TextField(db_column="Full Address", null=True, blank=True) street = models.TextField(db_column="Street", null=True, blank=True) city = models.TextField(db_column="City", null=True, blank=True) state_province = models.TextField(db_column="State/Province", null=True, blank=True) postal_code = models.TextField(db_column="Postal Code", null=True, blank=True) country = models.TextField(db_column="Country", null=True, blank=True) phone = models.TextField(db_column="Phone", null=True, blank=True) email = models.TextField(db_column="Email", null=True, blank=True) @classmethod def from_csv_dict(cls, data): txn = data.copy() # can't use '%s' format string, have to use the special function txn["_dp"] = datetime.fromtimestamp( txn["_dp"], tz=timezone.get_current_timezone() ) allowed_missing_fields = [ "sid", "uid", "eid", "fee", "sum", ] for field in allowed_missing_fields: if field not in txn: txn[field] = None return super().from_csv_dict(txn) _csv_headers_override = { "event_id": "eid", "timestamp": "_dp", "type": "Transaction Type", "for_what": "Event/Form Name", } def __str__(self): return f"{self.type} [{self.member if self.member else self.name}] {self.timestamp}" class Meta: db_table = "transactions"