diff --git a/backend/app/models/base.py b/backend/app/models/base.py index 01294ec..c1947f4 100644 --- a/backend/app/models/base.py +++ b/backend/app/models/base.py @@ -62,6 +62,11 @@ class ApiTags(DocumentedStrEnum): SERIES = "Series" +class VisitedCountType(DocumentedStrEnum): + NONE = auto_enum() + ONE_VISIT = auto_enum() + EACH_VISIT = auto_enum() + # endregion diff --git a/backend/app/models/mixin.py b/backend/app/models/mixin.py index 64ed681..d37300f 100644 --- a/backend/app/models/mixin.py +++ b/backend/app/models/mixin.py @@ -6,7 +6,7 @@ from sqlmodel import ( Field, ) -from .base import RowId as RowIdType +from .base import RowId as RowIdType, VisitedCountType from ..core.config import settings @@ -120,3 +120,28 @@ class Birthday(BaseModel): class Created(BaseModel): created_at: datetime | None = Field(nullable=False, default_factory=lambda: datetime.now(settings.tz_info)) created_by: RowIdType | None = Field(default=None, nullable=True, foreign_key="user.id", ondelete="SET NULL") + + +class QuestionInfo(BaseModel): + question_file: str | None = Field(default=None, nullable=True, max_length=255) + answer_file: str | None = Field(default=None, nullable=True, max_length=255) + + +class TeamAmmounts(BaseModel): + min_teams: int = Field(default=None, nullable=True, ge=1) + max_teams: int = Field(default=None, nullable=True, ge=1) + + +class MaxPoints(BaseModel): + max_points: int = Field(default=None, nullable=True, ge=0) + + +class VisitedPoints(BaseModel): + visited_points: int | None = Field( + default=None, + ge=0, + description="Visited points for this place, None will disable this function", + ) + visited_count_type: VisitedCountType = Field( + default=VisitedCountType.NONE, + ) diff --git a/backend/app/models/post.py b/backend/app/models/post.py new file mode 100644 index 0000000..bead817 --- /dev/null +++ b/backend/app/models/post.py @@ -0,0 +1,206 @@ +# app/models/post.py +from typing import TYPE_CHECKING + +from sqlmodel import Field, Relationship, Session, select + +from . import mixin +from .base import BaseSQLModel, RowId, DocumentedStrEnum, DocumentedIntFlag, auto_enum +from .user import PermissionRight, User + +if TYPE_CHECKING: + from .team import Team + +# region # Post ############################################################## + +class PostType(DocumentedIntFlag): + POINTS = auto_enum() #allows for the ability to get points + OPTION = auto_enum() #allows groups to enter waiting list + POINTS_OPTIONS = POINTS | POINTS + + VISITED = auto_enum() #only mark if a team has visited this post + CORRECT = auto_enum() #possible to fill in if correct (get max points) + + +class ReplayType(DocumentedStrEnum): + NOT_POSSIBLE = auto_enum() + ONLY_WHEN_NO_POINTS = auto_enum() + YES_BUT_FIRST_POINTS_COUNT = auto_enum() + YES_BUT_LAST_POINTS_COUNT = auto_enum() + YES_BUT_HIGHEST_POINTS_COUNT = auto_enum() + ALWAYS_AS_NEW = auto_enum() # add points together + +# ############################################################################## + +# Shared properties +class PostUserLinkBase(BaseSQLModel): + rights: PermissionRight = Field(default=PermissionRight.READ, nullable=False) + +# Properties to receive via API on creation +class PostUserLinkCreate(PostUserLinkBase): + user_id: RowId = Field(nullable=False) + +# Properties to receive via API on update, all are optional +class PostUserLinkUpdate(PostUserLinkBase): + pass + +# Database model (link tussen post en user) +class PostUserLink(PostUserLinkBase, table=True): + post_id: RowId = Field( + foreign_key="post.id", + primary_key=True, + nullable=False, + ondelete="CASCADE", + ) + user_id: RowId = Field( + foreign_key="user.id", + primary_key=True, + nullable=False, + ondelete="CASCADE", + ) + + post: "Post" = Relationship(back_populates="user_links") + user: "User" = Relationship(back_populates="post_links") + +# Properties to return via API +class PostUserLinkPublic(PostUserLinkBase): + user_id: RowId + post_id: RowId + +class PostUserLinksPublic(BaseSQLModel): + data: list[PostUserLinkPublic] + count: int + +# Shared properties +class PostBase( + #common + mixin.Name, #post name + mixin.Contact, #name contact person + mixin.IsActive, #scouting active (unused) + + #post specific + mixin.ShortName, #shortname for post + + mixin.MaxPoints, #max obtainable points + mixin.VisitedPoints, #points obtained for visiting post + + mixin.TeamAmmounts, #teams, min&max teams required + + mixin.QuestionInfo, #field for questions + + BaseSQLModel, +): + post_type: PostType = Field(default=PostType.POINTS, nullable=False) + replay: ReplayType = Field(default=ReplayType.NOT_POSSIBLE, nullable=False) + +# Properties to receive via API on creation +class PostCreate(PostBase): + pass + +# Properties to receive via API on update, all are optional +class PostUpdate(mixin.ShortNameUpdate, PostBase): + post_type: PostType | None = Field(default=None, nullable=True) # type: ignore + replay: ReplayType | None = Field(default=None, nullable=True) # type: ignore + +# Database model +class Post(mixin.RowId, PostBase, table=True): + + user_links: list["PostUserLink"] = Relationship(back_populates="post", cascade_delete=True) + #teams: list["Team"] = Relationship(back_populates="post", cascade_delete=True) + + @classmethod + def create(cls, *, session: Session, create_obj: PostCreate) -> "Post": + data_obj = create_obj.model_dump(exclude_unset=True) + db_obj = cls.model_validate(data_obj) + session.add(db_obj) + session.commit() + session.refresh(db_obj) + return db_obj + + @classmethod + def update(cls, *, session: Session, db_obj: "Post", in_obj: PostUpdate) -> "Post": + data_obj = in_obj.model_dump(exclude_unset=True) + db_obj.sqlmodel_update(data_obj) + session.add(db_obj) + session.commit() + session.refresh(db_obj) + return db_obj + + def get_user_link(self, user: User) -> "PostUserLink | None": + return next((link for link in self.user_links if link.user == user), None) + + def add_user( + self, + user: User, + rights: PermissionRight = PermissionRight.READ, + *, + session: Session, + ) -> "PostUserLink | None": + existing = self.get_user_link(user=user) + if existing is None: + new_link = PostUserLink(post=self, user=user, rights=rights) + self.user_links.append(new_link) + session.add(new_link) + session.commit() + return new_link + return None + + def update_user( + self, + user: User, + rights: PermissionRight = PermissionRight.READ, + *, + session: Session, + ) -> "PostUserLink | None": + link = self.get_user_link(user=user) + if link: + link.rights = rights + session.add(link) + session.commit() + return link + return None + + def remove_user(self, user: User, *, session: Session) -> None: + link = self.get_user_link(user=user) + if link: + session.delete(link) + session.commit() + + def user_has_rights( + self, + user: User, + rights: PermissionRight | None = None, + ) -> bool: + return any( + ( + link.user == user + and link.rights + and (not rights or (link.rights & rights) == rights) + ) + for link in self.user_links + ) + + def user_has_right( + self, + user: User, + rights: PermissionRight | None = None, + ) -> bool: + return any( + ( + link.user == user + and link.rights + and (not rights or (link.rights & rights)) + ) + for link in self.user_links + ) + + +# API output models +class PostPublic(mixin.RowIdPublic, PostBase): + pass + + +class PostsPublic(BaseSQLModel): + data: list[PostPublic] + count: int + +# endregion \ No newline at end of file diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 2611df8..edd756b 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -33,6 +33,7 @@ class PermissionModule(DocumentedStrEnum): DIVISION = auto_enum() MEMBER = auto_enum() SERIE = auto_enum() + POST = auto_enum() class PermissionPart(DocumentedStrEnum):