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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
**/.idea
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
3 changes: 3 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# output_encoding = utf-8

sqlalchemy.url = driver://user:pass@localhost/dbname
; sqlalchemy.url = asyncpg://postgres:postgres@localhost:5432/postgres
; postgresql://%(DB_USER)s:%(DB_PASS)s%(DB_HOST)s:%(DB_PORT)s/%(DB_NAME)s
; ENV FILE


[post_write_hooks]
Expand Down
36 changes: 36 additions & 0 deletions backend/my_app_api/models/models_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from datetime import datetime
from sqlalchemy import text
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy import JSON, Column, Integer


class Model(DeclarativeBase):
pass

# Создаем базу данных


class PostOrm(Model):
__tablename__ = 'post'
post_id: Mapped[int] = mapped_column(primary_key=True)

# Extra columns
title: Mapped[str]
picture_url: Mapped[str]
description: Mapped[str]
event_date: Mapped[datetime]
is_active: Mapped[bool]

created_at: Mapped[datetime] = mapped_column(
server_default=text("TIMEZONE('utc',now())"))
updated_at: Mapped[datetime] = mapped_column(
server_default=text("TIMEZONE('utc',now())"),
onupdate=datetime.utcnow)


class ReactionOrm(Model):
__tablename__ = 'reaction'
reaction_id: Mapped[int] = mapped_column(primary_key=True)
post_id: Mapped[int]
reaction: Mapped[int]
File renamed without changes.
35 changes: 35 additions & 0 deletions backend/my_app_api/orm/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

from my_app_api.settings import get_settings
from my_app_api.models.models_db import Model

settings = get_settings()


engine = create_async_engine(url=settings.database_url_asyncpg)

new_session = async_sessionmaker(engine) # Сессия для работы с БД


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with new_session() as session:
yield session


async def create_tables():
async with engine.begin() as conn:
await conn.run_sync(Model.metadata.create_all)


async def delete_tables():
async with engine.begin() as conn:
await conn.run_sync(Model.metadata.drop_all)


# def create_tables():
# Model.metadata.create_all(engine)


# def delete_tables():
# Model.metadata.drop_all(engine)
135 changes: 135 additions & 0 deletions backend/my_app_api/orm/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from my_app_api.utils.file_handle import safe_file
from my_app_api.utils.check_permission import check_permission
from sqlalchemy.dialects.postgresql import array
from fastapi import HTTPException, UploadFile
from sqlalchemy import delete, or_, select
from sqlalchemy.ext.asyncio import AsyncSession


from my_app_api.models.models_db import PostOrm
from my_app_api.shemas.posts import SPost, SPostAdd, SPostGetAll, SPostPatch
from my_app_api.orm.database import new_session
from my_app_api.utils.date_refactor import date_refactor


class PostRepository:
@staticmethod
async def get_one_post(session: AsyncSession, post_id: int, auth) -> SPost:
query = (
select(PostOrm)
.filter(PostOrm.post_id == post_id)
)
result = await session.execute(query)
result = result.scalars().first()
if not result:
raise HTTPException(
status_code=404, detail='Мероприятие не найдено')

banner = SPost.model_validate(
result, from_attributes=True)

return banner

@staticmethod
async def add_post(session: AsyncSession, data: SPostAdd, picture, auth) -> int:
check_permission(auth)

# refactor date
try:
data.event_date = date_refactor(data.event_date)
except:
raise HTTPException(
status_code=422, detail='Неверный формат даты')

query = (
select(PostOrm)
.where(PostOrm.title == data.title)
.where(PostOrm.event_date == data.event_date)
)
result = await session.execute(query)
if result.scalars().all():
raise HTTPException(
status_code=400, detail='Похожее мероприятие уже есть')

# Upload picture
picture_url = await safe_file(picture)

post_dict = data.model_dump()
post_dict.update({'picture_url': picture_url})

post = PostOrm(**post_dict)
session.add(post)
await session.flush()
post_id = post.post_id
await session.commit()
return post_id

@staticmethod
async def get_posts(session: AsyncSession, params: SPostGetAll, auth) -> list[SPost]:

query = select(PostOrm)

if params.offset is not None:
query = query.offset(params.offset)
if params.limit is not None:
query = query.limit(params.limit)

result = await session.execute(query)
post_models = result.scalars().all()

post_schemas = [SPost.model_validate(
post_orm, from_attributes=True) for post_orm in post_models]

return post_schemas

@staticmethod
async def delete_post(session: AsyncSession, post_id: int, auth):
check_permission(auth)

# Verify if banner is not exist
post = await session.get(PostOrm, (post_id, ))
if not post:
raise HTTPException(
status_code=404, detail='Мероприятие не найдено')

stmt = delete(PostOrm).filter(PostOrm.post_id == post_id)
await session.execute(stmt)
await session.commit()

@staticmethod
async def patch_post(session: AsyncSession, post_id: int, patch_post: SPostPatch, auth):
check_permission(auth)

post = await session.get(PostOrm, post_id)

# Verify 1 If banner by id exists
if post:
if patch_post.description is not None:
post.description = patch_post.description
if patch_post.title is not None:
post.title = patch_post.title
if patch_post.event_date is not None:
try:
post.event_date = date_refactor(patch_post.event_date)
except:
raise HTTPException(
status_code=422, detail='Неверный формат даты')
if patch_post.is_active is not None:
post.is_active = patch_post.is_active
else:
raise HTTPException(status_code=404, detail='Баннер не найден')

# Verify 2 If banner with same options exists
query = (
select(PostOrm)
.where(PostOrm.post_id != post.post_id)
.where(PostOrm.title == post.title)
.where(PostOrm.event_date == post.event_date)
)
result = await session.execute(query)
if result.scalars().all():
raise HTTPException(
status_code=400, detail='Похожее мероприятие уже есть')

await session.flush()
await session.commit()
22 changes: 18 additions & 4 deletions backend/my_app_api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,32 @@
from my_app_api.settings import get_settings
from starlette.datastructures import URL

from .touch import router as touch_router
from .posts import router as posts_router
from my_app_api.orm.database import delete_tables, create_tables


async def lifespan(app: FastAPI): # Дроп и создание БД при запуске приложения
try:
# await delete_tables()
await create_tables()
pass
except:
pass
yield
# await delete_tables()

settings = get_settings()
logger = logging.getLogger(__name__)

app = FastAPI(
title="Мое приложение",
description="Бэкэнд приложения-примера",
title="eventsMSU",
description="Лента мероприятий для студентов МГУ",
version=__version__,
# Отключаем нелокальную документацию
root_path=settings.ROOT_PATH if __version__ != "dev" else "",
docs_url="/swagger",
redoc_url=None,
lifespan=lifespan
)

app.add_middleware(
Expand Down Expand Up @@ -47,4 +61,4 @@ def redirect(request: Request):
return RedirectResponse(url)


app.include_router(touch_router)
app.include_router(posts_router)
71 changes: 71 additions & 0 deletions backend/my_app_api/routes/posts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging

from auth_lib.fastapi import UnionAuth
from my_app_api.orm.database import get_async_session
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, Depends, File, UploadFile
from typing import Annotated, Optional
from fastapi.responses import JSONResponse

from my_app_api.shemas.posts import SPostAdd, SPostGetAll, SPost, SPostPatch
from my_app_api.orm.repository import PostRepository

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/posts", tags=["Posts"])


@router.get("/{post_id}")
async def get_one_post(
session: Annotated[AsyncSession, Depends(get_async_session)],
post_id: int,
auth=Depends(UnionAuth(allow_none=False))
) -> SPost:
post = await PostRepository.get_one_post(session, post_id, auth)
return post


@router.post("/")
async def create_post(
session: Annotated[AsyncSession, Depends(get_async_session)],
post: Annotated[SPostAdd, Depends(SPostAdd)],
picture: Optional[UploadFile] = File(None),
auth=Depends(UnionAuth(allow_none=False))
) -> dict:
post_id = await PostRepository.add_post(session, post, picture, auth)
return JSONResponse(
status_code=201,
content={'post_id': post_id}
)


@router.get("/")
async def get_posts(
session: Annotated[AsyncSession, Depends(get_async_session)],
params: Annotated[SPostGetAll, Depends(SPostGetAll)],
auth=Depends(UnionAuth(allow_none=False))
) -> list[SPost]:
posts = await PostRepository.get_posts(session, params, auth)
return posts


@router.delete('/{post_id}')
async def delete_post(
session: Annotated[AsyncSession, Depends(get_async_session)],
post_id: int,
auth=Depends(UnionAuth(allow_none=False))
):
await PostRepository.delete_post(session, post_id, auth)


@router.patch('/{post_id}')
async def patch_post(
session: Annotated[AsyncSession, Depends(get_async_session)],
post_id: int,
post: Annotated[SPostPatch, Depends(SPostPatch)],
auth=Depends(UnionAuth(allow_none=False))
) -> dict:
await PostRepository.patch_post(session, post_id, post, auth)
return JSONResponse(
status_code=201,
content={'detail': 'ok'}
)
14 changes: 12 additions & 2 deletions backend/my_app_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@

class Settings(BaseSettings):
"""Application settings"""

DB_DSN: PostgresDsn = "postgresql://postgres@localhost:5432/postgres"
# DB_DSN: PostgresDsn = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
ROOT_PATH: str = "/" + os.getenv("APP_NAME", "")

CORS_ALLOW_ORIGINS: list[str] = ["*"]
CORS_ALLOW_CREDENTIALS: bool = True
CORS_ALLOW_METHODS: list[str] = ["*"]
CORS_ALLOW_HEADERS: list[str] = ["*"]

model_config = ConfigDict(case_sensitive=True, env_file=".env", extra="ignore")
@property
def database_url_psycopg(self):
return f'postgresql+psycopg2://postgres:postgres@events_db:5432/postgres'

@property
def database_url_asyncpg(self):
# return f'postgresql+asyncpg://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}'
return f'postgresql+asyncpg://postgres:postgres@events_db:5432/postgres'

model_config = ConfigDict(
case_sensitive=True, env_file=".env", extra="ignore")


@lru_cache
Expand Down
Empty file.
32 changes: 32 additions & 0 deletions backend/my_app_api/shemas/posts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from datetime import datetime
from pydantic import BaseModel


class SPostGetAll(BaseModel):
limit: int | None = None
offset: int | None = None


class SPostAdd(BaseModel):
title: str
description: str
event_date: str
is_active: bool = True


class SPost(BaseModel):
post_id: int
title: str
picture_url: str
description: str
is_active: bool
event_date: datetime
created_at: datetime
updated_at: datetime


class SPostPatch(BaseModel):
title: str | None = None
description: str | None = None
event_date: str | None = None
is_active: bool | None = None
Loading