"""
Global constants, constructs and exceptions for this project.
Constants and constructs that don't have an obvious home elsewhere are
defined here.
Non-Django-based exceptions are defined here.
"""
from dataclasses import dataclass, field
from datetime import date
from enum import Enum, IntEnum, auto
from typing import NamedTuple, List, Dict
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from fpiweb.models import Profile
# # # # # #
# Constants
# # # # # #
CURRENT_YEAR = date.today().year
""" The current year - used for validating expiration dates """
MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
MINIMUM_PASSWORD_LENGTH: int = 10
QR_LABELS_PER_PAGE = 12
QR_LABELS_MAX = 120
# # # # # # # # # # # # # # #
# Box Number Format Constants
#
# The box number format is "BOXnnnnn" where nnnnn is a five digit number.
# These canstants provide the programming constraints for validation.
# # # # # # # # # # # # # # #
BOX_PREFIX = 'BOX'
BOX_PREFIX_SIZE = len(BOX_PREFIX)
MIN_BOX_NUMBER = 1
MAX_BOX_NUMBER = 99999
MAX_BOX_NUMBER_DIGITS = len(str(MAX_BOX_NUMBER))
BOX_NUMBER_SIZE = BOX_PREFIX_SIZE + MAX_BOX_NUMBER_DIGITS
# # # # # # # # # # # #
# Decorator Definitions
# # # # # # # # # # # #
# use the following as a decorator for enums so templates will handle enums
# properly
[docs]def EnumForDjango(cls):
cls.do_not_call_in_templates = True
return cls
# # # # # # # # # # # # # #
# Special subclass of Enum
# # # # # # # # # # # # # #
[docs]class OrderedEnum(Enum):
"""
Enhanced Enum class that considers the members as ordered
"""
def __ge__(self, other):
if self.__class__ is other.__class__:
return self.value >= other.value
return NotImplemented
def __gt__(self, other):
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented
def __le__(self, other):
if self.__class__ is other.__class__:
return self.value <= other.value
return NotImplemented
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
# # # # # # # # # # # # # # #
# Project specific exceptions
# # # # # # # # # # # # # # #
[docs]class ProjectError(ValidationError):
"""
All exceptions unique to this project wil be based on this class.
"""
pass
[docs]class InvalidValueError(ProjectError):
"""
Used when an invalid value has been passed as a parameter.
"""
pass
[docs]class InvalidActionAttemptedError(ProjectError):
"""
A requested action cannot be done at this time.
"""
pass
[docs]class InternalError(ProjectError):
"""
The error is raised when there is some interal logic problem.
"""
[docs]@dataclass
class ValidOrErrorResponse:
"""
A constructed response denoting either valid or has error messages.
"""
is_valid: bool = True
error_msg_list: List[str] = field(default_factory=list)
[docs] def add_error(self, msg: str):
"""
Set as invalid and add an error message.
:param msg: error message to add
:return:
"""
self.is_valid = False
self.error_msg_list.append(msg)
return
def __repr__(self):
if self.is_valid:
display = "Valid"
else:
display = f'Invalid: {self.error_msg_list[0]}'
if len(self.error_msg_list) > 1:
for msg in self.error_msg_list[1:]:
display += f', {msg}'
return display
[docs]@EnumForDjango
class AccessLevel(OrderedEnum):
"""
Level of access for a user - lowest to highest.
"""
No_Access: int = 0
Volunteer: int = 10
Staff: int = 50
Admin: int = 99
def __str__(self) -> str:
display = f'{self.name} ({self.value})'
return display
# class AccessLevelName:
# """
# Names for strings representing the access levels.
# """
#
# PERM_VOLUNTEER = AccessLevel.Volunteer.name
# PERM_STAFF = AccessLevel.Staff.name
# PERM_ADMIN = AccessLevel.Admin.name
# AccessLevelDict = {text: level for text, level in
# AccessLevel.__members__.items()}
[docs]class AccessGroupsAndFlags:
"""
Unpacked attributes of an access level.
"""
access_level: AccessLevel = None
name: str = None
value: int = None
is_volunteer_group: bool = None
is_staff_group: bool = None
is_admin_group: bool = None
is_staff_flag: bool = None
is_superuser_flag: bool = None
def __init__(self, access_level: AccessLevel):
if access_level == AccessLevel.Volunteer:
self.access_level = AccessLevel['Volunteer']
self.name = self.access_level.name
self.value = self.access_level.value
self.is_volunteer_group = True
self.is_staff_group = False
self.is_admin_group = False
self.is_staff_flag = False
self.is_superuser_flag = False
elif access_level == AccessLevel.Staff:
self.access_level = AccessLevel['Staff']
self.name = self.access_level.name
self.value = self.access_level.value
self.is_volunteer_group = True
self.is_staff_group = True
self.is_admin_group = False
self.is_staff_flag = True
self.is_superuser_flag = False
elif access_level == AccessLevel.Admin:
self.access_level = AccessLevel['Admin']
self.name = self.access_level.name
self.value = self.access_level.value
self.is_volunteer_group = True
self.is_staff_group = True
self.is_admin_group = True
self.is_staff_flag = True
self.is_superuser_flag = True
else:
self.access_level = AccessLevel['No_Access']
self.name = self.access_level.name
self.value = self.access_level.value
self.is_volunteer_group = False
self.is_staff_group = False
self.is_admin_group = False
self.is_staff_flag = False
self.is_superuser_flag = False
return
def __str__(self) -> str:
"""
Provide a printable representation of entry.
:return: printable representation
"""
display = f'Access level: {self.name} ({self.value}), Groups: ' \
f'Vol-{"Y" if self.is_volunteer_group else "N"}, ' \
f'Staff-{"Y" if self.is_staff_group else "N"}, ' \
f'Admin-{"Y" if self.is_admin_group else "N"} ' \
f'Flags: Staff-{"Y" if self.is_staff_flag else "N"}, ' \
f'Super-{"Y" if self.is_superuser_flag else "N"}'
return display
[docs]def load_access_dict()-> Dict:
"""
Load a multipurpose dictionary with all access level identifieres.
:return: a dictionary with lots of keys
"""
ad = dict()
for access_level in AccessLevel:
access_group_flags = AccessGroupsAndFlags(access_level=access_level)
ad[access_level] = access_group_flags
ad[access_level.name] = access_group_flags
ad[access_level.value] = access_group_flags
ad[str(access_level)] = access_group_flags
return ad
AccessDict = load_access_dict()
[docs]class UserInfo(NamedTuple):
"""
Information about a specific user - abstracted from various tables.
"""
user: User
profile: Profile
highest_access_level: AccessLevel
is_active: bool
is_superuser: bool
[docs] def get_sort_key(self) -> str:
"""
Return a usable sort key
:return:
"""
fullname = self.user.get_full_name()
level = f'{self.highest_access_level.value:03}'
key = f'{level} {fullname}'
return key
[docs]class TargetUser(NamedTuple):
"""
User information to be added or updated.
"""
username: str = ''
# userpswd: str = ''
# confirm_pwd: str = ''
pswd_changed: bool = False
force_password: bool = False
first_name: str = ''
last_name: str = ''
email: str = ''
title: str = ''
access_level: AccessLevel = AccessLevel.No_Access
is_active: bool = True
# is_staff: bool = False
# is_superuser: bool = False
# EOF