Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions api_schemas/prereg_member_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from api_schemas.base_schema import BaseSchema
from pydantic_extra_types.phone_numbers import PhoneNumber


class PreregMemberCreate(BaseSchema):
telephone_number: PhoneNumber | None = None
stil_id: str | None = None
email: str | None = None


class PreregMemberUpdate(BaseSchema):
telephone_number: PhoneNumber | None = None
stil_id: str | None = None
email: str | None = None


class PreregMemberRead(BaseSchema):
prereg_member_id: int
telephone_number: PhoneNumber | None = None
stil_id: str | None = None
email: str | None = None
24 changes: 19 additions & 5 deletions api_schemas/user_schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING, Annotated, Literal
from pydantic import StringConstraints
from pydantic import StringConstraints, field_validator
from helpers.check_stil_id import check_stil_id
from fastapi_users_pelicanq import schemas as fastapi_users_schemas
from api_schemas.post_schemas import PostRead
from helpers.constants import MAX_FIRST_NAME_LEN, MAX_LAST_NAME_LEN
Expand All @@ -11,6 +12,19 @@
from api_schemas.group_schema import GroupRead


class StilIdValidationMixin:
stil_id: str | None = None

@field_validator("stil_id", mode="after")
@classmethod
def validate_stil_id(cls, value: str | None) -> str | None:
if value is None:
return None
if not check_stil_id(value):
raise ValueError("Invalid stil-id")
return value


class _UserEventRead(BaseSchema):
id: int

Expand Down Expand Up @@ -118,25 +132,25 @@ class UserInGroupRead(BaseSchema):


# fastapi-users will take all fields on this model and feed into the user constructor User_DB(...) when /auth/register route is called
class UserCreate(fastapi_users_schemas.BaseUserCreate, BaseSchema):
class UserCreate(StilIdValidationMixin, fastapi_users_schemas.BaseUserCreate, BaseSchema):
first_name: Annotated[str, StringConstraints(max_length=MAX_FIRST_NAME_LEN)]
last_name: Annotated[str, StringConstraints(max_length=MAX_LAST_NAME_LEN)]
telephone_number: PhoneNumber
start_year: int | None = None
pass
# stil_id is here, but inherited from StilIdValidationMixin


class UserUpdate(BaseSchema):
class UserUpdate(StilIdValidationMixin, BaseSchema):
first_name: str | None = None
last_name: str | None = None
start_year: int | None = None
program: PROGRAM_TYPE | None = None
notifications: bool | None = None
stil_id: str | None = None
standard_food_preferences: list[str] | None = None
other_food_preferences: str | None = None
telephone_number: PhoneNumber | None = None
moose_game_name: str | None = None
# stil_id is here, but inherited from StilIdValidationMixin


class UpdateUserMember(BaseSchema):
Expand Down
24 changes: 24 additions & 0 deletions db_models/prereg_member_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Optional
from sqlalchemy import CheckConstraint, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from .base_model import BaseModel_DB

from helpers.constants import MAX_TELEPHONE_LEN


class PreregMember_DB(BaseModel_DB):
__tablename__ = "prereg_member_table"
__table_args__ = (
CheckConstraint(
"telephone_number IS NOT NULL OR stil_id IS NOT NULL OR email IS NOT NULL",
name="at_least_one_identifier_required",
),
UniqueConstraint("telephone_number", "stil_id", "email", name="unique_prereg_member_identifiers"),
)
prereg_member_id: Mapped[int] = mapped_column(
primary_key=True, init=False
) # Not the id the user will have when confirmed as member!

telephone_number: Mapped[Optional[str]] = mapped_column(String(MAX_TELEPHONE_LEN), default=None)
stil_id: Mapped[Optional[str]] = mapped_column(default=None)
email: Mapped[Optional[str]] = mapped_column(default=None)
8 changes: 8 additions & 0 deletions helpers/check_stil_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import re


def check_stil_id(s: str) -> bool:
if not len(s) == 10:
return False
pattern = r"^[a-z]{2}\d{4}[a-z]{2}-s$"
return bool(re.fullmatch(pattern, s))
3 changes: 3 additions & 0 deletions routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .nomination_router import nomination_router
from .guild_meeting_router import guild_meeting_router
from .keyval_router import keyval_router
from .prereg_member_router import prereg_member_router

# here comes the big momma router
main_router = APIRouter()
Expand Down Expand Up @@ -97,3 +98,5 @@
main_router.include_router(guild_meeting_router, prefix="/guild-meeting", tags=["guild meeting"])

main_router.include_router(keyval_router, prefix="/keyvals", tags=["keyvals"])

main_router.include_router(prereg_member_router, prefix="/prereg-members", tags=["prereg members"])
196 changes: 196 additions & 0 deletions routes/prereg_member_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from fastapi import APIRouter, Body, HTTPException
from database import DB_dependency
from db_models.prereg_member_model import PreregMember_DB
from api_schemas.prereg_member_schema import (
PreregMemberCreate,
PreregMemberRead,
PreregMemberUpdate,
)
from user.permission import Permission
from helpers.check_stil_id import check_stil_id
import phonenumbers

prereg_member_router = APIRouter()


def normalize_telephone_number(telephone_number: str | None) -> str | None:
if telephone_number is None:
return None

return phonenumbers.format_number(
phonenumbers.parse(str(telephone_number), None), phonenumbers.PhoneNumberFormat.E164
)


def prereg_member_matches(member: PreregMember_DB, data: PreregMemberCreate | PreregMemberUpdate) -> bool:
return (
normalize_telephone_number(member.telephone_number) == normalize_telephone_number(data.telephone_number)
and member.stil_id == data.stil_id
and member.email == data.email
)


def validate_identifiers(data: PreregMemberCreate | PreregMemberUpdate) -> bool:
if data.stil_id:
if not check_stil_id(data.stil_id):
return False

if data.email:
if "@" not in data.email or "." not in data.email:
return False

return True


@prereg_member_router.get("/", response_model=list[PreregMemberRead], dependencies=[Permission.require("view", "User")])
def get_all_prereg_member_info(db: DB_dependency):
all_prereg_members = db.query(PreregMember_DB).all()
return all_prereg_members


@prereg_member_router.get(
"/{prereg_member_id}", response_model=PreregMemberRead, dependencies=[Permission.require("view", "User")]
)
def get_prereg_member_info(prereg_member_id: int, db: DB_dependency):
prereg_member = db.query(PreregMember_DB).filter(PreregMember_DB.prereg_member_id == prereg_member_id).one_or_none()
if not prereg_member:
raise HTTPException(404, detail="Prereg member not found")
return prereg_member


@prereg_member_router.post("/", response_model=PreregMemberRead, dependencies=[Permission.require("manage", "User")])
def create_prereg_member(
data: PreregMemberCreate,
db: DB_dependency,
):
if not (data.telephone_number or data.stil_id or data.email):
raise HTTPException(
400, detail="At least one identifier (telephone number, stil_id, or email) must be provided"
)

if not validate_identifiers(data):
raise HTTPException(400, detail="Invalid identifier(s) provided")

existing_prereg_member = next(
(member for member in db.query(PreregMember_DB).all() if prereg_member_matches(member, data)),
None,
)
if existing_prereg_member:
raise HTTPException(400, detail="Prereg member with the exact same details already exists")
prereg_member = PreregMember_DB(
telephone_number=data.telephone_number,
stil_id=data.stil_id,
email=data.email,
)
db.add(prereg_member)
db.commit()
return prereg_member


@prereg_member_router.post(
"/multiple",
response_model=list[PreregMemberRead],
dependencies=[Permission.require("manage", "User")],
)
def create_multiple_prereg_members(
data: list[PreregMemberCreate],
db: DB_dependency,
):
prereg_members: list[PreregMember_DB] = []
existing_prereg_members: list[PreregMember_DB] = []
for member_data in data:
if not (member_data.telephone_number or member_data.stil_id or member_data.email):
raise HTTPException(
400,
detail="At least one identifier (telephone number, stil_id, or email) must be provided for all entries you want to add",
)

if not validate_identifiers(member_data):
raise HTTPException(
400,
detail=f"One of the entries had an invalid identifier. At least one of the telephone number: {member_data.telephone_number}, stil_id: {member_data.stil_id}, or email: {member_data.email} is invalid",
)

existing_prereg_member = next(
(member for member in db.query(PreregMember_DB).all() if prereg_member_matches(member, member_data)),
None,
)

if existing_prereg_member:
# There is already an entry with the exact same combination of identifiers.
# Erroring here would be too strict
existing_prereg_members.append(existing_prereg_member)
continue

prereg_member = PreregMember_DB(
telephone_number=member_data.telephone_number,
stil_id=member_data.stil_id,
email=member_data.email,
)
db.add(prereg_member)
prereg_members.append(prereg_member)

if len(existing_prereg_members) >= 0 and len(existing_prereg_members) == len(data):
raise HTTPException(400, detail="All prereg members already exist")

db.commit()

# Note: returns all existing prereg members that would collide unless they were skipped, not the newly created prereg members
return existing_prereg_members


@prereg_member_router.patch(
"/{prereg_member_id}", response_model=PreregMemberRead, dependencies=[Permission.require("manage", "User")]
)
def update_prereg_member(prereg_member_id: int, data: PreregMemberUpdate, db: DB_dependency):
prereg_member = db.query(PreregMember_DB).filter(PreregMember_DB.prereg_member_id == prereg_member_id).one_or_none()
if not prereg_member:
raise HTTPException(404, detail="Prereg member not found")

if not validate_identifiers(data):
raise HTTPException(400, detail="Invalid identifier(s) provided")

for var, value in vars(data).items():
# Update the fields even if they are being set to None so they can be cleared
setattr(prereg_member, var, value)

if not (prereg_member.telephone_number or prereg_member.stil_id or prereg_member.email):
db.rollback()
raise HTTPException(
400, detail="At least one identifier (telephone number, stil_id, or email) must be provided"
)

db.commit()
return prereg_member


@prereg_member_router.delete(
"/multiple",
response_model=list[PreregMemberRead],
dependencies=[Permission.require("manage", "User")],
)
def delete_multiple_prereg_members(
db: DB_dependency,
prereg_member_ids: list[int] = Body(...),
):
prereg_members = db.query(PreregMember_DB).filter(PreregMember_DB.prereg_member_id.in_(prereg_member_ids)).all()
if not prereg_members:
raise HTTPException(404, detail="No prereg members found for the provided ids")
if len(prereg_members) != len(prereg_member_ids):
raise HTTPException(404, detail="Some prereg members not found for the provided ids")
for member in prereg_members:
db.delete(member)
db.commit()
return prereg_members


@prereg_member_router.delete(
"/{prereg_member_id}", response_model=PreregMemberRead, dependencies=[Permission.require("manage", "User")]
)
def delete_prereg_member(prereg_member_id: int, db: DB_dependency):
prereg_member = db.query(PreregMember_DB).filter(PreregMember_DB.prereg_member_id == prereg_member_id).one_or_none()
if not prereg_member:
raise HTTPException(404, detail="Prereg member not found")
db.delete(prereg_member)
db.commit()
return prereg_member
9 changes: 1 addition & 8 deletions services/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@
from db_models.user_model import User_DB
from fastapi import HTTPException, status
from sqlalchemy.exc import DataError, NoResultFound, MultipleResultsFound
import re
from helpers.types import FOOD_PREFERENCES
from db_models.post_model import Post_DB


def check_stil_id(s: str) -> bool:
if not len(s) == 10:
return False
pattern = r"^[a-z]{2}\d{4}[a-z]{2}-s$"
return bool(re.fullmatch(pattern, s))
from helpers.check_stil_id import check_stil_id


def condition(model, asset):
Expand Down
Loading
Loading