Source code for accounts.models

""" Models for our definitions of a user within the system. """
import random
import string
from datetime import date

from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe

from cobalt.settings import (
    AUTO_TOP_UP_MAX_AMT,
    GLOBAL_ORG,
    TBA_PLAYER,
    RBAC_EVERYONE,
    ABF_USER,
    API_KEY_PREFIX,
)
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, RegexValidator
from django.db import models, transaction


[docs] def no_future(value): today = date.today() if value > today: raise ValidationError("Date cannot be in the future.")
[docs] class User(AbstractUser): """ User class based upon AbstractUser. """
[docs] class CovidStatus(models.TextChoices): UNSET = "US", "Unset" USER_CONFIRMED = "UC", "User Confirmed" ADMIN_CONFIRMED = "AC", "Administrator Confirmed" USER_EXEMPT = "AV", "User Medically Exempt from Vaccination"
email = models.EmailField(unique=False) system_number = models.IntegerField( "%s Number" % GLOBAL_ORG, blank=True, unique=True, db_index=True, ) deceased = models.BooleanField("Deceased", default=False) """ Player is deceased, status set by My ABF support """ phone_regex = RegexValidator( # regex=r"^\+?1?\d{9,15}$", regex=r"^04\d{8}$", message="We only accept Australian phone numbers starting 04 which are 10 numbers long.", ) mobile = models.CharField( "Mobile Number", blank=True, unique=True, null=True, max_length=15, validators=[phone_regex], ) about = models.TextField("About Me", blank=True, null=True, max_length=800) pic = models.ImageField( upload_to="pic_folder/", default="pic_folder/default-avatar.png" ) dob = models.DateField(blank="True", null=True, validators=[no_future]) bbo_name = models.CharField("BBO Username", blank=True, null=True, max_length=20) auto_amount = models.PositiveIntegerField( "Auto Top Up Amount", blank=True, null=True, validators=[MaxValueValidator(AUTO_TOP_UP_MAX_AMT)], ) stripe_customer_id = models.CharField( "Stripe Customer Id", blank=True, null=True, max_length=25 ) AUTO_STATUS = [ ("Off", "Off"), ("Pending", "Pending"), ("On", "On"), ] stripe_auto_confirmed = models.CharField( "Stripe Auto Confirmed", max_length=9, choices=AUTO_STATUS, default="Off" ) system_number_search = models.BooleanField( "Show %s number on searches" % GLOBAL_ORG, default=True ) receive_sms_results = models.BooleanField("Receive SMS Results", default=True) receive_email_results = models.BooleanField( "Receive Results by Email", default=True ) receive_sms_reminders = models.BooleanField("Receive SMS Reminders", default=False) receive_abf_newsletter = models.BooleanField("Receive ABF Newsletter", default=True) receive_marketing = models.BooleanField("Receive Marketing", default=True) receive_monthly_masterpoints_report = models.BooleanField( "Receive Monthly Masterpoints Report", default=True ) receive_payments_emails = models.BooleanField( "Receive Payments Emails", default=True ) receive_low_balance_emails = models.BooleanField(default=True) windows_scrollbar = models.BooleanField( "Use Perfect Scrollbar on Windows", default=False ) last_activity = models.DateTimeField(blank="True", null=True) covid_status = models.CharField( choices=CovidStatus.choices, max_length=2, default=CovidStatus.UNSET ) REQUIRED_FIELDS = [ "system_number", "email", ] # tells createsuperuser to ask for them def __str__(self): if self.id in (TBA_PLAYER, RBAC_EVERYONE, ABF_USER): return self.first_name else: return "%s (%s: %s)" % (self.full_name, GLOBAL_ORG, self.system_number) @property def full_name(self): """Returns the person's full name.""" return "%s %s" % (self.first_name, self.last_name) @property def href(self): """Returns an HTML link tag that can be used to go to the users public profile""" url = reverse("accounts:public_profile", kwargs={"pk": self.id}) return format_html( "<a href='{}' target='_blank'>{}</a>", mark_safe(url), self.full_name )
[docs] class UnregisteredUserManager(models.Manager): """ Manager to return a query set of unregistered users with non-internal system numbers only """
[docs] def get_queryset(self): return ( super() .get_queryset() .filter( internal_system_number=False, ) )
[docs] class UnregisteredUser(models.Model): """Represents users who we have only partial information about and who have not registered themselves yet. When a User registers, the matching instance of Unregistered User will be removed. Email addresses are a touchy subject as some clubs believe they own this information and do not want it shared with other clubs. We protect email address by having another model (UnregisteredUserEmail) that is organisation specific. The email address in this model is from the MPC (considered "public" although it is not shown to anyone), while the other email address is "private" to the club that provided it, but ironically shown to the club that did and editable. """ # Import here to avoid circular dependencies from organisations.models import Organisation ORIGINS = [ ("MPC", "Masterpoints Centre Import"), ("Pianola", "Pianola Import"), ("CSV", "CSV Import"), ("Manual", "Manual Entry"), ] system_number = models.IntegerField( "%s Number" % GLOBAL_ORG, unique=True, db_index=True, ) first_name = models.CharField("First Name", max_length=150, blank=True, null=True) last_name = models.CharField("Last Name", max_length=150, blank=True, null=True) origin = models.CharField("Origin", choices=ORIGINS, max_length=10) deceased = models.BooleanField("Deceased", default=False) """ Player is deceased, status set by My ABF support """ internal_system_number = models.BooleanField(default=False) last_updated_by = models.ForeignKey( User, on_delete=models.PROTECT, related_name="last_updated" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) last_registration_invite_sent = models.DateTimeField( "Last Registration Invite Sent", blank=True, null=True ) last_registration_invite_by_user = models.ForeignKey( User, on_delete=models.PROTECT, blank=True, null=True ) last_registration_invite_by_club = models.ForeignKey( Organisation, on_delete=models.PROTECT, blank=True, null=True, related_name="last_registration_invite_by_club", ) added_by_club = models.ForeignKey( Organisation, on_delete=models.PROTECT, blank=True, null=True, related_name="added_by_club", ) identifier = models.CharField( max_length=10, default="NOTSET", ) """ random string identifier to use in emails to handle preferences. Can't use the pk obviously """ # Managers: objects excludes internal system number records, all_objects does not all_objects = models.Manager() objects = UnregisteredUserManager()
[docs] def save(self, *args, **kwargs): """create identifier on first save""" if not self.pk: self.identifier = "".join( random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10) ) super(UnregisteredUser, self).save(*args, **kwargs)
def __str__(self): if self.internal_system_number: return f"{self.full_name} (No {GLOBAL_ORG} number)" else: return f"{self.full_name} ({GLOBAL_ORG}: {self.system_number})" @property def full_name(self): """Returns the person's full name.""" return f"{self.first_name} {self.last_name}"
[docs] class TeamMate(models.Model): """link two members together""" user = models.ForeignKey( User, on_delete=models.CASCADE, related_name="team_mate_user" ) team_mate = models.ForeignKey( User, on_delete=models.CASCADE, related_name="team_mate_team_mate" ) make_payments = models.BooleanField("Use my account", default=False) def __str__(self): if self.make_payments: return f"Plus - {self.user.full_name} - {self.team_mate.full_name}" else: return f"Basic - {self.user.full_name} - {self.team_mate.full_name}"
[docs] class UserPaysFor(models.Model): """Allow a user to charge their bridge to another person"""
[docs] class Circumstance(models.TextChoices): ALWAYS = "AL", "Always" IF_PLAYING_TOGETHER = "PT", "If Playing Together" IF_PLAYING_SAME_SESSION = "PS", "If Playing Same Session"
sponsor = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sponsor") lucky_person = models.ForeignKey( User, on_delete=models.CASCADE, related_name="lucky_person" ) criterion = models.CharField( max_length=2, choices=Circumstance.choices, default=Circumstance.ALWAYS ) def __str__(self): return f"{self.sponsor.full_name} pays for {self.lucky_person.full_name}"
def _create_api_token(): string_size = 40 - len(API_KEY_PREFIX) random_string = "".join( random.SystemRandom().choice( string.ascii_letters + string.digits + "!$^()-_{}|/" ) for _ in range(string_size) ) return f"{API_KEY_PREFIX}{random_string}"
[docs] class APIToken(models.Model): """API Tokens map to a user and are used by any API functions We don't put an expiry on the token but this could be added later if required.""" user = models.ForeignKey(User, on_delete=models.CASCADE) token = models.CharField( max_length=40, default="Overridden on save", help_text="This is set when you first save it", ) created_date = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.user} - {self.created_date}"
[docs] def save(self, *args, **kwargs): """Create token on first save""" if len(self.token) != 40: self.token = _create_api_token() super(APIToken, self).save(*args, **kwargs)
[docs] class UserAdditionalInfo(models.Model): """Additional information about a user that is not regularly accessed. The intention is to move all of the extras from the User class into here over time as the User is getting overloaded and is accessed constantly by Django so we should try to keep it clean. """ user = models.ForeignKey(User, on_delete=models.CASCADE) email_hard_bounce = models.BooleanField(default=False) """ Set this flag if we get a hard bounce from sending an email """ email_hard_bounce_reason = models.TextField(null=True, blank=True) """ Reason for the bounce """ email_hard_bounce_date = models.DateTimeField(null=True, blank=True) congress_view_filters = models.CharField(max_length=200, blank=True) """ user preferences for the congress listing page """ member_sort_order = models.CharField(max_length=20, blank=True) """ sort order for the club menu members tab list """ last_club_visited = models.IntegerField(null=True, blank=True) """ used to store which club was last visited for users with access to multiple clubs """ def __str__(self): return self.user.__str__()
[docs] class NextInternalSystemNumber(models.Model): """A singleton table to manage the next internal system number All access should be through the next_available class method: system_number = NextInternalSystemNumber.next_available() This will return the number to be used, update the stored value and take a row level update lock until the end of the transaction. NOTE: As this is holding a lock, make sure that the transaction is completed (comitted or rolled back) as quickly as possible. """ _first_number = 1_000_000_000 number = models.IntegerField("Next Internal System Number", default=_first_number)
[docs] def save(self, *args, **kwargs): self.pk = 1 super(NextInternalSystemNumber, self).save(*args, **kwargs)
[docs] def delete(self, *args, **kwargs): pass
[docs] @classmethod def load(cls): """Load the singleton, taking an update lock Must be called within a transaction""" try: obj = cls.objects.select_for_update().get(pk=1) except cls.DoesNotExist: obj = cls() obj.number = cls._first_number obj.save() obj = cls.objects.select_for_update().get(pk=1) return obj
[docs] @classmethod def next_available(cls): """Returns the next available internal system number Note that this takes an update lock on the singleton until the end of teh outermost transaction""" nisn = cls.load() allocated_number = nisn.number nisn.number += 1 nisn.save() return allocated_number
[docs] @classmethod def is_internal(cls, number): """Checks whether the number is an internal system number""" return number >= cls._first_number
[docs] class SystemCard(models.Model): """System cards for users"""
[docs] class SystemClassification(models.TextChoices): GREEN = "G" BLUE = "B" YELLOW = "Y" RED = "R"
# Meta data user = models.ForeignKey(User, on_delete=models.CASCADE) card_name = models.CharField(max_length=100) save_date = models.DateTimeField(auto_now=True) # Basic Info player1 = models.CharField(max_length=100, blank=True) player2 = models.CharField(max_length=100, blank=True) basic_system = models.CharField(max_length=50, default="Standard American") system_classification = models.CharField( max_length=1, choices=SystemClassification.choices, default=SystemClassification.GREEN, ) brown_sticker = models.BooleanField(default=False) brown_sticker_why = models.CharField(max_length=50, blank=True) canape = models.BooleanField(default=False) # Openings opening_1c = models.CharField(max_length=20, blank=True) opening_1d = models.CharField(max_length=20, blank=True) opening_1h = models.CharField(max_length=20, blank=True) opening_1s = models.CharField(max_length=20, blank=True) opening_1nt = models.CharField(max_length=20, blank=True) # Summary summary_bidding = models.CharField(max_length=100, blank=True) summary_carding = models.CharField(max_length=100, blank=True) # Pre-alerts pre_alerts = models.TextField(blank=True) # 1NT Responses nt1_response_2c = models.CharField(max_length=20, blank=True) nt1_response_2d = models.CharField(max_length=20, blank=True) nt1_response_2h = models.CharField(max_length=20, blank=True) nt1_response_2s = models.CharField(max_length=20, blank=True) nt1_response_2nt = models.CharField(max_length=20, blank=True) # 2 Level Openings opening_2c = models.CharField(max_length=20, blank=True) opening_2d = models.CharField(max_length=20, blank=True) opening_2h = models.CharField(max_length=20, blank=True) opening_2s = models.CharField(max_length=20, blank=True) opening_2nt = models.CharField(max_length=20, blank=True) # Higher Openings opening_3nt = models.CharField(max_length=20, blank=True) opening_other = models.CharField(max_length=20, blank=True) # Competitive bids competitive_doubles = models.CharField(max_length=100, blank=True) competitive_lead_directing_doubles = models.CharField(max_length=100, blank=True) competitive_jump_overcalls = models.CharField(max_length=100, blank=True) competitive_unusual_nt = models.CharField(max_length=100, blank=True) competitive_1nt_overcall_immediate = models.CharField(max_length=20, blank=True) competitive_1nt_overcall_reopening = models.CharField(max_length=20, blank=True) competitive_negative_double_through = models.CharField(max_length=20, blank=True) competitive_responsive_double_through = models.CharField(max_length=20, blank=True) competitive_immediate_cue_bid_minor = models.CharField(max_length=100, blank=True) competitive_immediate_cue_bid_major = models.CharField(max_length=100, blank=True) competitive_weak_2_defense = models.CharField(max_length=100, blank=True) competitive_weak_3_defense = models.CharField(max_length=100, blank=True) competitive_transfer_defense = models.CharField(max_length=100, blank=True) competitive_nt_defense = models.CharField(max_length=100, blank=True) # Basic Responses basic_response_jump_raise_minor = models.CharField(max_length=100, blank=True) basic_response_jump_raise_major = models.CharField(max_length=100, blank=True) basic_response_jump_shift_minor = models.CharField(max_length=100, blank=True) basic_response_jump_shift_major = models.CharField(max_length=100, blank=True) basic_response_to_2c_opening = models.CharField(max_length=100, blank=True) basic_response_to_strong_2_opening = models.CharField(max_length=100, blank=True) basic_response_to_2nt_opening = models.CharField(max_length=100, blank=True) # Carding - suit play_suit_lead_sequence = models.CharField(max_length=100, blank=True) play_suit_lead_4_or_more = models.CharField(max_length=100, blank=True) play_suit_lead_4_small = models.CharField(max_length=100, blank=True) play_suit_lead_3 = models.CharField(max_length=100, blank=True) play_suit_lead_in_partners_suit = models.CharField(max_length=100, blank=True) play_suit_discards = models.CharField(max_length=100, blank=True) play_suit_count = models.CharField(max_length=100, blank=True) play_suit_signal_on_partner_lead = models.CharField(max_length=100, blank=True) # Carding - NT play_nt_lead_sequence = models.CharField(max_length=100, blank=True) play_nt_lead_4_or_more = models.CharField(max_length=100, blank=True) play_nt_lead_4_small = models.CharField(max_length=100, blank=True) play_nt_lead_3 = models.CharField(max_length=100, blank=True) play_nt_lead_in_partners_suit = models.CharField(max_length=100, blank=True) play_nt_discards = models.CharField(max_length=100, blank=True) play_nt_count = models.CharField(max_length=100, blank=True) play_nt_signal_on_partner_lead = models.CharField(max_length=100, blank=True) play_signal_declarer_lead = models.CharField(max_length=100, blank=True) play_notes = models.CharField(max_length=100, blank=True) # Slams slam_conventions = models.CharField(max_length=200, blank=True) # Other other_conventions = models.CharField(max_length=200, blank=True) # Responses # 1C response_1c_1d = models.CharField(max_length=20, blank=True) response_1c_1h = models.CharField(max_length=20, blank=True) response_1c_1s = models.CharField(max_length=20, blank=True) response_1c_1n = models.CharField(max_length=20, blank=True) response_1c_2c = models.CharField(max_length=20, blank=True) response_1c_2d = models.CharField(max_length=20, blank=True) response_1c_2h = models.CharField(max_length=20, blank=True) response_1c_2s = models.CharField(max_length=20, blank=True) response_1c_2n = models.CharField(max_length=20, blank=True) response_1c_3c = models.CharField(max_length=20, blank=True) response_1c_3d = models.CharField(max_length=20, blank=True) response_1c_3h = models.CharField(max_length=20, blank=True) response_1c_3s = models.CharField(max_length=20, blank=True) response_1c_3n = models.CharField(max_length=20, blank=True) response_1c_other = models.CharField(max_length=100, blank=True) # 1D response_1d_1h = models.CharField(max_length=20, blank=True) response_1d_1s = models.CharField(max_length=20, blank=True) response_1d_1n = models.CharField(max_length=20, blank=True) response_1d_2c = models.CharField(max_length=20, blank=True) response_1d_2d = models.CharField(max_length=20, blank=True) response_1d_2h = models.CharField(max_length=20, blank=True) response_1d_2s = models.CharField(max_length=20, blank=True) response_1d_2n = models.CharField(max_length=20, blank=True) response_1d_3c = models.CharField(max_length=20, blank=True) response_1d_3d = models.CharField(max_length=20, blank=True) response_1d_3h = models.CharField(max_length=20, blank=True) response_1d_3s = models.CharField(max_length=20, blank=True) response_1d_3n = models.CharField(max_length=20, blank=True) response_1d_other = models.CharField(max_length=100, blank=True) # 1H response_1h_1s = models.CharField(max_length=20, blank=True) response_1h_1n = models.CharField(max_length=20, blank=True) response_1h_2c = models.CharField(max_length=20, blank=True) response_1h_2d = models.CharField(max_length=20, blank=True) response_1h_2h = models.CharField(max_length=20, blank=True) response_1h_2s = models.CharField(max_length=20, blank=True) response_1h_2n = models.CharField(max_length=20, blank=True) response_1h_3c = models.CharField(max_length=20, blank=True) response_1h_3d = models.CharField(max_length=20, blank=True) response_1h_3h = models.CharField(max_length=20, blank=True) response_1h_3s = models.CharField(max_length=20, blank=True) response_1h_3n = models.CharField(max_length=20, blank=True) response_1h_other = models.CharField(max_length=100, blank=True) # 1S response_1s_1n = models.CharField(max_length=20, blank=True) response_1s_2c = models.CharField(max_length=20, blank=True) response_1s_2d = models.CharField(max_length=20, blank=True) response_1s_2h = models.CharField(max_length=20, blank=True) response_1s_2s = models.CharField(max_length=20, blank=True) response_1s_2n = models.CharField(max_length=20, blank=True) response_1s_3c = models.CharField(max_length=20, blank=True) response_1s_3d = models.CharField(max_length=20, blank=True) response_1s_3h = models.CharField(max_length=20, blank=True) response_1s_3s = models.CharField(max_length=20, blank=True) response_1s_3n = models.CharField(max_length=20, blank=True) response_1s_other = models.CharField(max_length=100, blank=True) # 1N response_1n_3c = models.CharField(max_length=20, blank=True) response_1n_3d = models.CharField(max_length=20, blank=True) response_1n_3h = models.CharField(max_length=20, blank=True) response_1n_3s = models.CharField(max_length=20, blank=True) response_1n_3n = models.CharField(max_length=20, blank=True) response_1n_other = models.CharField(max_length=100, blank=True) # 2c response_2c_2d = models.CharField(max_length=20, blank=True) response_2c_2h = models.CharField(max_length=20, blank=True) response_2c_2s = models.CharField(max_length=20, blank=True) response_2c_2n = models.CharField(max_length=20, blank=True) response_2c_3c = models.CharField(max_length=20, blank=True) response_2c_3d = models.CharField(max_length=20, blank=True) response_2c_3h = models.CharField(max_length=20, blank=True) response_2c_3s = models.CharField(max_length=20, blank=True) response_2c_3n = models.CharField(max_length=20, blank=True) response_2c_other = models.CharField(max_length=100, blank=True) # 2d response_2d_2h = models.CharField(max_length=20, blank=True) response_2d_2s = models.CharField(max_length=20, blank=True) response_2d_2n = models.CharField(max_length=20, blank=True) response_2d_3c = models.CharField(max_length=20, blank=True) response_2d_3d = models.CharField(max_length=20, blank=True) response_2d_3h = models.CharField(max_length=20, blank=True) response_2d_3s = models.CharField(max_length=20, blank=True) response_2d_3n = models.CharField(max_length=20, blank=True) response_2d_other = models.CharField(max_length=100, blank=True) # 2h response_2h_2s = models.CharField(max_length=20, blank=True) response_2h_2n = models.CharField(max_length=20, blank=True) response_2h_3c = models.CharField(max_length=20, blank=True) response_2h_3d = models.CharField(max_length=20, blank=True) response_2h_3h = models.CharField(max_length=20, blank=True) response_2h_3s = models.CharField(max_length=20, blank=True) response_2h_3n = models.CharField(max_length=20, blank=True) response_2h_other = models.CharField(max_length=100, blank=True) # 2s response_2s_2n = models.CharField(max_length=20, blank=True) response_2s_3c = models.CharField(max_length=20, blank=True) response_2s_3d = models.CharField(max_length=20, blank=True) response_2s_3h = models.CharField(max_length=20, blank=True) response_2s_3s = models.CharField(max_length=20, blank=True) response_2s_3n = models.CharField(max_length=20, blank=True) response_2s_other = models.CharField(max_length=100, blank=True) # 2NT response_2n_3c = models.CharField(max_length=20, blank=True) response_2n_3d = models.CharField(max_length=20, blank=True) response_2n_3h = models.CharField(max_length=20, blank=True) response_2n_3s = models.CharField(max_length=20, blank=True) response_2n_3n = models.CharField(max_length=20, blank=True) response_2n_other = models.CharField(max_length=100, blank=True) # notes response_notes = models.CharField(max_length=200, blank=True) other_notes = models.CharField(max_length=400, blank=True) def __str__(self): local_datetime = timezone.localtime(self.save_date) return f"{self.user.full_name} - {self.card_name} - {local_datetime:%a %-d %b %Y %I:%M%p}"