Skip to content

Commit

Permalink
Merge pull request #30 from wafflestudio/vote_image
Browse files Browse the repository at this point in the history
Vote image
  • Loading branch information
morecleverer authored Jan 17, 2025
2 parents 0af995a + de42f37 commit 6738359
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 17 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
echo "DB_PORT=${{ secrets.DB_PORT }}" >> .env.prod
echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> .env.prod
echo "SECRET_FOR_JWT=${{ secrets.SECRET_FOR_JWT }}" >> .env.prod
echo "SERVER_IP=${{ secrets.SERVER_IP }}" >> .env.prod
- name: Get Public IP
id: ip
Expand Down Expand Up @@ -70,6 +71,7 @@ jobs:
fi &&
sudo docker run -d --name snuvote -p 8000:8000 \
--env-file /home/${{ secrets.SERVER_USER }}/.env.prod \
--mount type=bind,source=/home/ubuntu/snuvote_images,target=/src/images \
odumag99/snuvote:${{ env.VERSION }}"
- name: Remove GitHub Actions IP
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ __pycache__
.venv
.vscode
.env.prod
/images
2 changes: 2 additions & 0 deletions snuvote/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from snuvote.app.user.views import user_router
from snuvote.app.vote.views import vote_router
from snuvote.app.image.views import image_router

api_router = APIRouter()

api_router.include_router(user_router, prefix="/users", tags=["users"])
api_router.include_router(vote_router, prefix="/votes", tags=["votes"])
api_router.include_router(image_router, prefix="/images", tags=["images"])
8 changes: 8 additions & 0 deletions snuvote/app/image/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter
from fastapi.responses import FileResponse

image_router = APIRouter()

@image_router.get("/{image_name}")
def get_image(image_name: str) -> FileResponse:
return FileResponse(f'./images/{image_name}')
1 change: 1 addition & 0 deletions snuvote/app/vote/dto/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,5 @@ class VoteDetailResponse(BaseModel):
end_datetime: Annotated[datetime, AfterValidator(convert_utc_to_ktc_naive)] # UTC 시간대를 KST 시간대로 변환한 뒤 offset-naive로 변환
choices: List[ChoiceDetailResponse]
comments: List[CommentDetailResponse]
images: List[str]

33 changes: 28 additions & 5 deletions snuvote/app/vote/service.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
from typing import Annotated, List

from fastapi import Depends
from fastapi import Depends, UploadFile
from snuvote.database.models import Vote, User, Choice, ChoiceParticipation, Comment
from snuvote.app.vote.store import VoteStore
from snuvote.app.vote.errors import ChoiceNotFoundError, InvalidFieldFormatError, MultipleChoicesError, ParticipationCodeError, ParticipationCodeNotProvidedError, WrongParticipationCodeError, EndedVoteError, CommentNotYoursError, CommentNotInThisVoteError
from snuvote.app.vote.dto.requests import ParticipateVoteRequest, CommentRequest

from datetime import datetime, timedelta, timezone


import secrets
import os

class VoteService:
def __init__(self, vote_store: Annotated[VoteStore, Depends()]) -> None:
self.vote_store = vote_store

async def upload_vote_images(self, vote: Vote, images: List[UploadFile]) -> None:
# voteimage를 저장하고 DB에 정보를 저장하는 함수
image_order = 0
for image in images:
image_order += 1
image_name = f'{secrets.token_urlsafe(16)}.{image.filename.split(".")[-1]}'
image_path = f'./images/{image_name}' # 임시로 도커 컨테이너 '/src/images'에 저장
with open(image_path, 'wb') as f:
f.write(await image.read())
image_src = f'http://{os.getenv("SERVER_IP")}:8000/api/images/{image_name}' # 이미지를 불러올 수 있는 URL
self.vote_store.add_vote_image(vote_id=vote.id, image_order=image_order, image_src=image_src)


#투표 추가하기
def add_vote(self,
async def add_vote(self,
writer_id:int,
title: str,
content: str,
Expand All @@ -25,13 +39,15 @@ def add_vote(self,
multiple_choice:bool,
annonymous_choice:bool,
end_datetime:datetime,
choices: List[str]) -> Vote:
choices: List[str],
images: List[UploadFile]) -> Vote:

#참여코드가 필요한데 참여코드가 없을 경우 400 에러
if participation_code_required and not participation_code:
raise ParticipationCodeError()

return self.vote_store.add_vote(writer_id=writer_id,
# 투표 추가
vote = self.vote_store.add_vote(writer_id=writer_id,
title=title,
content=content,
participation_code_required=participation_code_required,
Expand All @@ -41,6 +57,13 @@ def add_vote(self,
annonymous_choice=annonymous_choice,
end_datetime=end_datetime,
choices=choices)

# 이미지 업로드
if images:
await self.upload_vote_images(vote, images)

return self.vote_store.get_vote_by_vote_id(vote_id=vote.id)


# 진행 중인 투표 리스트 조회
def get_ongoing_list(self, start_cursor: datetime|None) -> tuple[List[Vote], bool, datetime|None]:
Expand Down
12 changes: 10 additions & 2 deletions snuvote/app/vote/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime, timedelta, timezone

from fastapi import Depends
from snuvote.database.models import Vote, Choice, ChoiceParticipation, Comment
from snuvote.database.models import Vote, Choice, ChoiceParticipation, Comment, VoteImage

from snuvote.database.connection import get_db_session
from sqlalchemy import select, delete
Expand Down Expand Up @@ -51,10 +51,18 @@ def add_vote(self,
choice_content = choice_content_input)
self.session.add(choice)

self.session.commit()
self.session.flush()

return vote

def add_vote_image(self, vote_id: int, image_order: int, image_src: str):
new_voteimage = VoteImage(vote_id = vote_id,
order=image_order,
src = image_src)

self.session.add(new_voteimage)
self.session.flush()

# 진행 중인 투표 리스트 조회
def get_ongoing_list(self, start_cursor: datetime|None) -> tuple[List[Vote], bool, datetime|None]:

Expand Down
23 changes: 14 additions & 9 deletions snuvote/app/vote/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Header
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from typing import Annotated, List
from fastapi import APIRouter, Depends, File, UploadFile, Form
from fastapi.security import HTTPBearer

from pydantic.functional_validators import AfterValidator
from starlette.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED
Expand All @@ -21,13 +21,16 @@

#create vote
@vote_router.post("/create", status_code=HTTP_201_CREATED)
def create_vote(
async def create_vote(
user: Annotated[User, Depends(login_with_access_token)],
create_vote_request: CreateVoteRequest,
vote_service: Annotated[VoteService, Depends()]
vote_service: Annotated[VoteService, Depends()],
images: List[UploadFile]|None = File(None),
create_vote_json = Form(media_type="multipart/form-data", json_schema_extra=CreateVoteRequest.model_json_schema())
):
create_vote_request = CreateVoteRequest.model_validate_json(create_vote_json)


vote = vote_service.add_vote(
vote = await vote_service.add_vote(
writer_id=user.id,
title=create_vote_request.title,
content=create_vote_request.content,
Expand All @@ -37,7 +40,8 @@ def create_vote(
multiple_choice=create_vote_request.multiple_choice,
annonymous_choice=create_vote_request.annonymous_choice,
end_datetime=create_vote_request.end_datetime,
choices=create_vote_request.choices
choices=create_vote_request.choices,
images = images
)

return get_vote(vote.id, user, vote_service)
Expand Down Expand Up @@ -88,7 +92,8 @@ def get_vote(
create_datetime = vote.create_datetime,
end_datetime = vote.end_datetime,
choices= [ChoiceDetailResponse.from_choice(choice, user, vote.annonymous_choice, vote.realtime_result) for choice in vote.choices],
comments = [CommentDetailResponse.from_comment_user(comment, user) for comment in vote.comments if comment.is_deleted==False]
comments = [CommentDetailResponse.from_comment_user(comment, user) for comment in vote.comments if comment.is_deleted==False],
images = [image.src for image in vote.images]
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""VoteImage 추가
Revision ID: 9e2715d6e171
Revises: 0fdb97db69f3
Create Date: 2025-01-17 03:22:31.557304
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '9e2715d6e171'
down_revision: Union[str, None] = '0fdb97db69f3'
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.create_table('vote_image',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('vote_id', sa.BigInteger(), nullable=False),
sa.Column('order', sa.Integer(), nullable=False),
sa.Column('src', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['vote_id'], ['vote.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('vote_image')
# ### end Alembic commands ###
16 changes: 15 additions & 1 deletion snuvote/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Vote(Base):

comments: Mapped[Optional[List["Comment"]]] = relationship("Comment", back_populates="vote", uselist=True)

images: Mapped[Optional[List["VoteImage"]]] = relationship("VoteImage", back_populates="vote", uselist=True)

class Choice(Base):
__tablename__ = "choice"

Expand Down Expand Up @@ -92,4 +94,16 @@ class BlockedRefreshToken(Base):
__tablename__ = "blocked_refresh_token"

token_id: Mapped[str] = mapped_column(String(255), primary_key=True)
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)

class VoteImage(Base):
__tablename__ = "vote_image"

id: Mapped[int] = mapped_column(BigInteger, primary_key=True)

vote_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("vote.id"))
vote: Mapped["Vote"] = relationship("Vote", back_populates="images", uselist=False)

order: Mapped[int] = mapped_column(Integer, nullable=False)

src: Mapped[str] = mapped_column(String(255), nullable=False)
3 changes: 3 additions & 0 deletions snuvote/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError

from snuvote.api import api_router
from snuvote.app.user.errors import MissingRequiredFieldError

load_dotenv(dotenv_path = '.env.prod')

app = FastAPI()

app.include_router(api_router, prefix="/api")
Expand Down

0 comments on commit 6738359

Please sign in to comment.