Source code for app.models.member_model

import re
from typing import List, TYPE_CHECKING

import bcrypt
from sqlalchemy.orm import Mapped, mapped_column, validates, relationship

from app.extensions import db
from app.utils import is_valid_datestring

if TYPE_CHECKING:
    from app.schemas.member_schema import MemberSchema
    from app.models.project_participation_model import ProjectParticipation
    # from app.models.point_model import Point


def _hash_password(password) -> str:
    # salted encrypted password
    hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
    return hashed.decode("utf-8")


[docs] class Member(db.Model): __tablename__ = "members" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(unique=True) name: Mapped[str] = mapped_column() email: Mapped[str] = mapped_column() ist_id: Mapped[str] = mapped_column(unique=True, nullable=True) _password: Mapped[str | None] = mapped_column("password", nullable=True) _roles: Mapped[str] = mapped_column("roles") member_number: Mapped[int] = mapped_column(nullable=True) course: Mapped[str] = mapped_column(nullable=True) join_date: Mapped[str] = mapped_column(nullable=True) exit_date: Mapped[str] = mapped_column(nullable=True) description: Mapped[str] = mapped_column(nullable=True) extra: Mapped[str] = mapped_column(nullable=True) project_participations: Mapped[List["ProjectParticipation"]] = relationship("ProjectParticipation", back_populates="member", cascade="all, delete-orphan", passive_deletes=True)
[docs] @classmethod def from_schema(self, schema: "MemberSchema"): return self(**schema.model_dump())
def __init__(self, *, ist_id=None, username=None, name=None, email=None, password=None, member_number=None, course=None, roles=None, join_date=None, exit_date=None, description=None, extra=None): self.ist_id = ist_id self.username = username self.name = name self.email = email self.password = password self.member_number = member_number self.course = course self.roles = roles self.join_date = join_date self.exit_date = exit_date self.description = description self.extra = extra @property def password(self) -> str: return self._password @password.setter def password(self, v: str): if v is None: self._password = None return if not isinstance(v, str): raise ValueError(f"Invalid password type: '{type(v)}'") if not 6 <= len(v) <= 256: raise ValueError("Invalid password length, minimum 6 and maximum 256 characters") self._password = _hash_password(v) @property def roles(self) -> List[str]: if "," not in self._roles: return [] if self._roles == "" else [self._roles] return self._roles.split(",") @roles.setter def roles(self, v: List[str]): if v is None: self._roles = "" return if not isinstance(v, list): raise ValueError(f"Invalid roles type: '{type(v)}'") self._roles = ",".join(v)
[docs] @validates("ist_id") def validate_ist_id(self, k, v): if v is None: return None if not isinstance(v, str): raise ValueError(f"Invalid IST ID type: '{type(v)}'") if not re.match(r"^ist1[0-9]{5,7}$", v): raise ValueError(f"Invalid IST ID pattern: '{v}'") return v
[docs] @validates("username") def validate_username(self, k, v): if not isinstance(v, str): raise ValueError(f"Invalid username type: '{type(v)}'") if not isinstance(v, str) or not 3 <= len(v) <= 32: raise ValueError(f"Invalid username length, minimum 3 and maximum 32 characters: '{v}'") if not re.match(r"^[a-zA-Z0-9]*$", v): raise ValueError(f"Invalid characters in username: '{v}'") return v
[docs] @validates("name", "email") def validate_name(self, k, v): if not isinstance(v, str): raise ValueError(f"Invalid {k} type: '{type(v)}'") if not (1 <= len(v) <= 256): raise ValueError(f"Invalid {k} length, minimum 1 and maximum 256 characters: '{v}'") return v
[docs] @validates("course") def validate_course(self, k, v): if v is None: return None if not isinstance(v, str): raise ValueError(f"Invalid course type: '{type(v)}'") if not (1 <= len(v) <= 8): raise ValueError(f"Invalid course length, minimum 1 and maximum 8 characters: '{v}'") return v
[docs] @validates("member_number") def validate_member_number(self, k, v): if v is None: return None if not isinstance(v, int): raise ValueError(f"Invalid member_number type: '{type(v)}'") if v < 1: raise ValueError(f"Invalid member_number, expected integer greater than 0: Got {v}") return v
[docs] @validates("join_date", "exit_date") def validate_datestring(self, k, v): if v is None: return None if not isinstance(v, str): raise ValueError(f"Invalid {k} type: '{type(v)}'") if not is_valid_datestring(v): raise ValueError(f"Invalid {k} format, expected 'YYYY-MM-DD': '{v}'") return v
[docs] @validates("description", "extra") def validate_description(self, k, v): if v is None: return None if not isinstance(v, str): raise ValueError(f"Invalid {k} type: '{type(v)}'") if len(v) > 2048: raise ValueError(f"Invalid {k} length, minimum 0 and maximum 2048 characters: '{v}'") return v
[docs] def matches_password(self, password: str): return bcrypt.checkpw(password.encode("utf-8"), self.password.encode("utf-8"))
def __repr__(self): return f"<{self.__class__.__name__}({', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())})>"