diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e22ca81..90af90d 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -30,35 +30,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - # - name: Check Alembic Migration Version - # id: alembicChecker - # uses: DevGlitch/alembic-migration-checker@v1.1 - # with: - # db_url: ${{ secrets.DB_URL }} - # migrations_path: ./migrations/ - - # - name: check Alembic migration - # id: alembicChecker - # uses: appleboy/ssh-action@v1.0.3 - # with: - # host: ${{ secrets.SSH_HOST }} - # username: ${{ secrets.SSH_USER }} - # password: ${{ secrets.SSH_PASSCODE }} - # port: ${{ secrets.SSH_PORT }} - # script: | - # cd /diskb/home/backend - # git pull origin develop - # . /diskb/home/backend/.venv/bin/activate - # alembic heads > heads_output.txt - # alembic current > current_output.txt - # if cmp -s heads_output.txt current_output.txt; then - # echo "No new migrations to apply." - # echo "new_migrations=false" >> $GITHUB_ENV - # else - # echo "New migrations found." - # echo "new_migrations=true" >> $GITHUB_ENV - # fi - - name: executing remote ssh commands using password uses: appleboy/ssh-action@v1.0.3 diff --git a/migrations/versions/64a70f848243_forum_global.py b/migrations/versions/64a70f848243_forum_global.py new file mode 100644 index 0000000..20e6962 --- /dev/null +++ b/migrations/versions/64a70f848243_forum_global.py @@ -0,0 +1,35 @@ +"""forum_global + +Revision ID: 64a70f848243 +Revises: 49bd2f582a5a +Create Date: 2024-11-21 22:36:19.346377 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '64a70f848243' +down_revision: Union[str, None] = '49bd2f582a5a' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('forumpost', 'class_id', + existing_type=sa.INTEGER(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('forumpost', 'class_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### diff --git a/server/exception_handlers.py b/server/exception_handlers.py index 6d567c8..81afdb0 100644 --- a/server/exception_handlers.py +++ b/server/exception_handlers.py @@ -24,4 +24,4 @@ async def handle_google_auth_error(request, exc: GoogleAuthError) -> JSONRespons def add_exception_handlers(app: FastAPI) -> None: app.add_exception_handler(IntegrityError, handle_integrity_error) # type: ignore [arg-type] app.add_exception_handler(NoResultFound, handle_no_result_found) # type: ignore [arg-type] - app.add_exception_handler(GoogleAuthError, handle_google_auth_error) + app.add_exception_handler(GoogleAuthError, handle_google_auth_error) # type: ignore [arg-type] diff --git a/server/models/database/forum_db_model.py b/server/models/database/forum_db_model.py index 7dafac3..7f7717b 100644 --- a/server/models/database/forum_db_model.py +++ b/server/models/database/forum_db_model.py @@ -11,8 +11,9 @@ class ForumPost(SQLModel, table=True): id: int = Field(default=None, primary_key=True) - class_id: int = Field( - foreign_key="class.id" + class_id: int | None = Field( + foreign_key="class.id", + nullable=True ) subject_id : int = Field( diff --git a/server/models/http/requests/forum_request_models.py b/server/models/http/requests/forum_request_models.py index 5652794..70b6a9d 100644 --- a/server/models/http/requests/forum_request_models.py +++ b/server/models/http/requests/forum_request_models.py @@ -6,7 +6,7 @@ class ForumPostRegister(BaseModel): user_id: int content: str - class_id: int + class_id: int | None = None subject_id: int filter_tags: list[int] | None = None @@ -42,4 +42,4 @@ class ForumPostLike(BaseModel): like_state: bool class ForumUserLikesRegister(BaseModel): - user_id: int \ No newline at end of file + user_id: int diff --git a/server/models/http/responses/forum_post_response.py b/server/models/http/responses/forum_post_response.py index 202bd48..24b644c 100644 --- a/server/models/http/responses/forum_post_response.py +++ b/server/models/http/responses/forum_post_response.py @@ -9,7 +9,7 @@ class ForumPostResponse(BaseModel): user_id: int user_name: str content : str | None - class_id : int + class_id : int | None subject_id: int created_at : datetime replies_count: int @@ -40,7 +40,7 @@ class ForumPostReplyResponse(ForumPostResponse): reply_of_post_id: int | None @classmethod - def from_forum_reply(cls, reply: ForumPost, mobile_user_id: int, session: Session) -> "ForumPostReplyResponse": + def from_forum_reply(cls, reply: ForumPost, mobile_user_id: int | None, session: Session) -> "ForumPostReplyResponse": return cls( id = reply.id, @@ -57,5 +57,5 @@ def from_forum_reply(cls, reply: ForumPost, mobile_user_id: int, session: Sessio ) @classmethod - def from_forum_post_reply_list(cls, replies: list[ForumPost], mobile_user_id: int, session: Session) -> list["ForumPostReplyResponse"]: + def from_forum_post_reply_list(cls, replies: list[ForumPost], mobile_user_id: int | None, session: Session) -> list["ForumPostReplyResponse"]: return [cls.from_forum_reply(reply, mobile_user_id, session) for reply in replies] diff --git a/server/repositories/forum_repository.py b/server/repositories/forum_repository.py index b35346c..18e5ce5 100644 --- a/server/repositories/forum_repository.py +++ b/server/repositories/forum_repository.py @@ -37,13 +37,14 @@ def get_post_like_reaction(mobile_user_id: int | None, post_id: int, session:Ses return user_liked_this_post @staticmethod - def get_all_posts(*, subject_id: int, filter_tags: list[int] | None, session: Session) -> list[ForumPost]: + def get_all_posts(*, subject_id: int, filter_tags: list[int] | None, search_keyword: str | None,session: Session) -> list[ForumPost]: """Returns all posts matching, ordered by desceding creation date (newer first), the filter_tags criteria as follows:\n - Each filter tags must be an prime number (e.g.: 2, 3, 5...) - Multiple filter tags will be the multiplication of corresponding tag numbers: (e.g.: 10 = 2*5 - tags 2 and 5) - If a post has no filter, its filter_tags will be 1 (not a prime multiplication) - To search, all posts with ANY filter_tags combination received from the request are going to be returned - e.g.:filter_tags = [2, 5, 3], from request, means posts with the following filter_tags will be returned: [2, 5, 3, 10, 6, 15, 30] + Return posts filtered by keyword, if it was found in the request """ statement = select(ForumPost).where( col(ForumPost.subject_id)==subject_id, @@ -51,6 +52,14 @@ def get_all_posts(*, subject_id: int, filter_tags: list[int] | None, session: Se col(ForumPost.enabled) == True) \ .order_by(col(ForumPost.created_at).desc()) + if search_keyword != None: + search_term = f"%{search_keyword}%" + statement = select(ForumPost).where( + col(ForumPost.subject_id)==subject_id, + col(ForumPost.enabled) == True, + ForumPost.content.ilike(search_term)) \ + .order_by(col(ForumPost.created_at).desc()) + if filter_tags != None: # if there are filter tags, use them to search filter_expressions = [] @@ -181,7 +190,6 @@ def change_forum_post_like( return post - class PostNotFoundException(HTTPException): def __init__(self, post_id: int) -> None: super().__init__( diff --git a/server/repositories/subject_repository.py b/server/repositories/subject_repository.py index 983f097..8042908 100644 --- a/server/repositories/subject_repository.py +++ b/server/repositories/subject_repository.py @@ -1,10 +1,9 @@ -from ast import Sub +from datetime import date from fastapi import HTTPException, status -from sqlalchemy.exc import IntegrityError, NoResultFound +from sqlalchemy.exc import NoResultFound from sqlmodel import Session, col, select from server.deps.authenticate import BuildingDep -from server.deps.session_dep import SessionDep from server.models.database.subject_building_link import SubjectBuildingLink from server.models.database.subject_db_model import Subject from server.models.http.requests.subject_request_models import ( @@ -14,6 +13,7 @@ from server.repositories.building_repository import BuildingRepository from server.repositories.calendar_repository import CalendarRepository from server.services.jupiter_crawler.crawler import JupiterCrawler +from server.utils.enums.subject_type import SubjectType class SubjectRepository: @@ -145,6 +145,31 @@ def delete(*, id: int, session: Session) -> None: session.delete(subject) session.commit() + @staticmethod + def create_general_forum(*,id: int, name: str, session: Session) -> Subject: + new_subject = Subject( + id=id, + name=name, + code="", + professors=[], + type= SubjectType.OTHER, + work_credit=0, + class_credit=0, + activation=date.today(), + ) + session.add(new_subject) + session.commit() + session.refresh(new_subject) + return new_subject + + @staticmethod + def get_by_name(*, name: str, session: Session) -> Subject: + statement = select(Subject).where(Subject.name == name) + try: + subject = session.exec(statement).one() + return subject + except NoResultFound: + raise SubjectNotFound() class SubjectNotFound(HTTPException): def __init__(self) -> None: diff --git a/server/routes/public/forum_routes.py b/server/routes/public/forum_routes.py index 0eb6960..b07be05 100644 --- a/server/routes/public/forum_routes.py +++ b/server/routes/public/forum_routes.py @@ -4,6 +4,7 @@ from server.deps.session_dep import SessionDep from server.models.database.forum_db_model import ForumPost +from server.models.database.subject_db_model import Subject from server.models.http.requests.forum_request_models import ( ForumPostRegister, to_forumpost_model, @@ -16,6 +17,7 @@ ForumPostResponse, ) from server.repositories.forum_repository import ForumRepository +from server.repositories.subject_repository import SubjectNotFound, SubjectRepository from server.utils.google_auth_utils import authenticate_with_google from server.services.gmail_service import gmail_login, gmail_send_message @@ -29,26 +31,40 @@ async def get_posts( session: SessionDep, subject_id: int, user_id: int | None = None, - filter_tags: List[int] = Query(None) -) -> list[ForumPostResponse]: - """Get all posts by tags, also gets all posts reaction information based on the user_id""" + filter_tags: List[int] = Query(None), + search_keyword: str | None = None +) -> list[ForumPostReplyResponse]: + """Get all posts by tags, also gets all posts reaction information based on the user_id + If a search keyword is present in the request, return posts with that word + """ posts = ForumRepository.get_all_posts( subject_id=subject_id, filter_tags=filter_tags, + search_keyword=search_keyword, session=session ) - return ForumPostResponse.from_forum_post_list(user_id, posts, session) + return ForumPostReplyResponse.from_forum_post_reply_list(posts, user_id, session) @router.post("/posts") async def create_forum_post( input: ForumPostRegister, session: SessionDep, authorization: str = Header(None), ) -> ForumPostResponse: - """Create a forum post with provided tags""" + """Create a forum post with provided tags. The General Forum is associeted with subject_id == -1""" # authenticate before creating and saving a post authenticate_with_google(authorization) + if input.subject_id == -1: + globalForumSubject: Subject + try: + globalForumSubject = SubjectRepository.get_by_name(name="Forum Geral", session=session) + except SubjectNotFound: + globalForumSubject = SubjectRepository.create_general_forum(id=input.subject_id, name="Forum Geral", session=session) + + input.subject_id = globalForumSubject.id # type: ignore + input.class_id = None + forum_post = ForumRepository.create( input=to_forumpost_model(input), session=session @@ -139,5 +155,3 @@ async def update_forum_post_like( like_changed_post = ForumRepository.change_forum_post_like(post_id=post_id, mobile_user_id=input.user_id, like_state=input.like_state ,session=session, ) return like_changed_post - -