Skip to content
Merged
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
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
run:
source ./venv/bin/activate && uvicorn --reload --log-level debug calendar_backend.routes.base:app
source ./venv/bin/activate && uvicorn --reload --log-config logging_dev.conf calendar_backend.routes.base:app

configure: venv
source ./venv/bin/activate && pip install -r requirements.dev.txt -r requirements.txt
Expand All @@ -8,12 +8,12 @@ venv:
python3.11 -m venv venv

format:
autoflake -r --in-place --remove-all-unused-imports ./calendar_backend
isort ./calendar_backend
black ./calendar_backend
source ./venv/bin/activate && autoflake -r --in-place --remove-all-unused-imports ./calendar_backend
source ./venv/bin/activate && isort ./calendar_backend
source ./venv/bin/activate && black ./calendar_backend

db:
docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-timetable_api postgres:15

migrate:
alembic upgrade head
source ./venv/bin/activate && alembic upgrade head
67 changes: 67 additions & 0 deletions calendar_backend/methods/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio
import os
import random
import string
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from io import BytesIO
from typing import Final

import aiofiles
from fastapi import File, HTTPException, UploadFile
from PIL import Image
from sqlalchemy.orm import Session

from calendar_backend.models.db import ApproveStatuses, Lecturer, Photo
from calendar_backend.settings import get_settings


SUPPORTED_FILE_EXTENSIONS: Final[list[str]] = ['png', 'svg', 'jpg', 'jpeg']
settings = get_settings()


async def upload_lecturer_photo(lecturer_id: int, session: Session, file: UploadFile = File(...)) -> Photo:
lecturer = Lecturer.get(lecturer_id, session=session)
random_string = ''.join(random.choice(string.ascii_letters) for _ in range(32))
ext = file.filename.split('.')[-1]
if ext not in SUPPORTED_FILE_EXTENSIONS:
raise HTTPException(status_code=422, detail="Unsupported file extension")
filename = f"{random_string}.{ext}"
path = os.path.join(settings.STATIC_PATH, "photo", "lecturer", filename)
async with aiofiles.open(path, 'wb') as out_file:
content = await file.read()
await async_image_process(content)
await out_file.write(content)
approve_status = ApproveStatuses.APPROVED if not settings.REQUIRE_REVIEW_PHOTOS else ApproveStatuses.PENDING
photo = Photo(
lecturer_id=lecturer_id,
link=filename,
approve_status=approve_status,
)
session.add(photo)
session.flush()
lecturer.avatar_id = lecturer.last_photo.id if lecturer.last_photo else lecturer.avatar_id
session.flush()
return photo


def process_image(image_bytes: bytes) -> None:
with Image.open(BytesIO(image_bytes)) as image:
try:
image.verify()
except SyntaxError:
raise HTTPException(status_code=422, detail="Corrupted file")


thread_pool = ThreadPoolExecutor()


async def async_image_process(image_bytes: bytes) -> None:
loop = asyncio.get_event_loop()
await loop.run_in_executor(thread_pool, partial(process_image, image_bytes))


def get_photo_webpath(file_path: str):
file_path = file_path.removeprefix('/')
root_path = settings.ROOT_PATH.removesuffix('/')
return f"{root_path}/static/photo/lecturer/{file_path}"
58 changes: 1 addition & 57 deletions calendar_backend/methods/utils.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import asyncio
import datetime
import os
import random
import string
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from io import BytesIO
from typing import Final

import aiofiles
from fastapi import File, HTTPException, UploadFile
from PIL import Image
from sqlalchemy.orm import Session

from calendar_backend.models.db import ApproveStatuses, Event, Group, Lecturer, Photo, Room
from calendar_backend.models.db import Event, Group, Lecturer, Room
from calendar_backend.settings import get_settings


Expand Down Expand Up @@ -67,46 +54,3 @@ async def get_lecturer_lessons_in_daterange(
if lesson.start_ts.date() >= date_start and lesson.end_ts.date() < date_end:
events_list.append(lesson)
return events_list


SUPPORTED_FILE_EXTENSIONS: Final[list[str]] = ['png', 'svg', 'jpg', 'jpeg']


async def upload_lecturer_photo(lecturer_id: int, session: Session, file: UploadFile = File(...)) -> Photo:
lecturer = Lecturer.get(lecturer_id, session=session)
random_string = ''.join(random.choice(string.ascii_letters) for _ in range(32))
ext = file.filename.split('.')[-1]
if ext not in SUPPORTED_FILE_EXTENSIONS:
raise HTTPException(status_code=422, detail="Unsupported file extension")
path = os.path.join(settings.STATIC_PATH, "photo", "lecturer", f"{random_string}.{ext}")
async with aiofiles.open(path, 'wb') as out_file:
content = await file.read()
await async_image_process(content)
await out_file.write(content)
approve_status = ApproveStatuses.APPROVED if not settings.REQUIRE_REVIEW_PHOTOS else ApproveStatuses.PENDING
photo = Photo(
lecturer_id=lecturer_id,
link=path,
approve_status=approve_status,
)
session.add(photo)
session.flush()
lecturer.avatar_id = lecturer.last_photo.id if lecturer.last_photo else lecturer.avatar_id
session.flush()
return photo


def process_image(image_bytes: bytes) -> None:
with Image.open(BytesIO(image_bytes)) as image:
try:
image.verify()
except SyntaxError:
raise HTTPException(status_code=422, detail="Corrupted file")


thread_pool = ThreadPoolExecutor()


async def async_image_process(image_bytes: bytes) -> None:
loop = asyncio.get_event_loop()
await loop.run_in_executor(thread_pool, partial(process_image, image_bytes))
6 changes: 1 addition & 5 deletions calendar_backend/routes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
lecturer_comment_review_router as old_lecturer_comment_review_router,
) # DEPRICATED TODO: Drop 2023-04-01
from .lecturer import lecturer_comment_router as old_lecturer_comment_router # DEPRICATED TODO: Drop 2023-04-01
from .lecturer import (
lecturer_photo_review_router as old_lecturer_photo_review_router,
) # DEPRICATED TODO: Drop 2023-04-01
from .lecturer import lecturer_photo_router as old_lecturer_photo_router # DEPRICATED TODO: Drop 2023-04-01
from .lecturer import lecturer_router as old_lecturer_router # DEPRICATED TODO: Drop 2023-04-01
from .lecturer.comment import router as lecturer_comment_router
Expand Down Expand Up @@ -119,7 +116,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
app.add_middleware(
DBSessionMiddleware,
db_url=settings.DB_DSN,
engine_args={"pool_pre_ping": True},
engine_args={"pool_pre_ping": True, "isolation_level": "AUTOCOMMIT"},
)
app.add_middleware(
CORSMiddleware,
Expand All @@ -139,7 +136,6 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
app.include_router(old_lecturer_comment_router)
app.include_router(old_lecturer_comment_review_router)
app.include_router(old_lecturer_photo_router)
app.include_router(old_lecturer_photo_review_router)
app.include_router(old_group_router)
app.include_router(old_room_router)
app.include_router(old_event_router)
Expand Down
13 changes: 1 addition & 12 deletions calendar_backend/routes/gcal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from functools import lru_cache
from urllib.parse import unquote

from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from fastapi import APIRouter, BackgroundTasks, HTTPException
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
from fastapi_sqlalchemy import db
Expand Down Expand Up @@ -39,17 +39,6 @@ def get_flow(state=""):
)


@gcal.get("/")
async def home(request: Request):
groups = [
f"{row.number}, {row.name}" if row.name else f"{row.number}" for row in db.session.query(Group).filter().all()
]
return templates.TemplateResponse(
"index.html",
{"request": request, "groups": groups},
)


@gcal.get("/flow")
async def get_user_flow(state: str):
if settings.GOOGLE_CLIENT_SECRET:
Expand Down
2 changes: 0 additions & 2 deletions calendar_backend/routes/lecturer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
from .comment_review import lecturer_comment_review_router
from .lecturer import lecturer_router
from .photo import lecturer_photo_router
from .photo_review import lecturer_photo_review_router


__all__ = [
"lecturer_photo_review_router",
"lecturer_comment_review_router",
"lecturer_comment_router",
"lecturer_router",
Expand Down
34 changes: 24 additions & 10 deletions calendar_backend/routes/lecturer/lecturer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
from typing import Any
from typing import Any, Literal

from auth_lib.fastapi import UnionAuth
from fastapi import APIRouter, Depends
from fastapi_sqlalchemy import db
from sqlalchemy.orm import Query, joinedload

from calendar_backend.exceptions import ObjectNotFound
from calendar_backend.methods.image import get_photo_webpath
from calendar_backend.models.db import ApproveStatuses, Lecturer
from calendar_backend.models.db import Photo as DbPhoto
from calendar_backend.routes.models import GetListLecturer, LecturerGet, LecturerPatch, LecturerPost
Expand All @@ -23,9 +25,10 @@
@router.get("/{id}", response_model=LecturerGet)
async def get_lecturer_by_id(id: int) -> LecturerGet:
lecturer = Lecturer.get(id, session=db.session)
result = LecturerGet.from_orm(Lecturer.get(id, session=db.session))
if lecturer.avatar_id:
lecturer.avatar_link = lecturer.avatar.link
return LecturerGet.from_orm(Lecturer.get(id, session=db.session))
result.avatar_link = get_photo_webpath(lecturer.avatar.link)
return result


@lecturer_router.get("/", response_model=GetListLecturer) # DEPRICATED TODO: Drop 2023-04-01
Expand All @@ -34,15 +37,26 @@ async def get_lecturers(
query: str = "",
limit: int = 10,
offset: int = 0,
order_by: Literal['first_name', 'last_name'] | None = None,
) -> dict[str, Any]:
res = Lecturer.get_all(session=db.session).filter(Lecturer.search(query))
query: Query = Lecturer.get_all(session=db.session).filter(Lecturer.search(query))
query = query.options(joinedload(Lecturer.avatar)) # Сразу загружаем аватарки
if order_by:
query = query.order_by(order_by)
query = query.order_by('id')
if limit:
cnt, res = res.count(), res.offset(offset).limit(limit).all()
cnt, query = query.count(), query.offset(offset).limit(limit)
else:
cnt, res = res.count(), res.offset(offset).all()
for row in res:
row.avatar_link = row.avatar.link if row.avatar else None
result = [LecturerGet.from_orm(row) for row in res]
cnt, query = query.count(), query.offset(offset)
query = query.all()
logger.debug(query)

result = []
for row in query:
row_get = LecturerGet.from_orm(row)
if row.avatar:
row_get.avatar_link = get_photo_webpath(row.avatar.link)
result.append(row_get)
return {
"items": result,
"limit": limit,
Expand Down Expand Up @@ -71,7 +85,7 @@ async def patch_lecturer(
if photo.lecturer_id != id or photo.approve_status != ApproveStatuses.APPROVED:
raise ObjectNotFound(DbPhoto, lecturer_inp.avatar_id)
lecturer_upd = Lecturer.update(
id, session=db.session, **lecturer_inp.dict(exclude_unset=True), avatar_link=photo.link
id, session=db.session, **lecturer_inp.dict(exclude_unset=True), avatar_link=get_photo_webpath(photo.link)
)
else:
lecturer_upd = Lecturer.update(id, session=db.session, **lecturer_inp.dict(exclude_unset=True))
Expand Down
13 changes: 10 additions & 3 deletions calendar_backend/routes/lecturer/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
from fastapi_sqlalchemy import db

from calendar_backend.exceptions import ObjectNotFound
from calendar_backend.methods import utils
from calendar_backend.methods.image import get_photo_webpath, upload_lecturer_photo
from calendar_backend.models.db import ApproveStatuses, Lecturer
from calendar_backend.models.db import Photo as DbPhoto
from calendar_backend.routes.models import LecturerPhotos, Photo
from calendar_backend.settings import get_settings


settings = get_settings()
# DEPRICATED TODO: Drop 2023-04-01
lecturer_photo_router = APIRouter(prefix="/timetable/lecturer/{lecturer_id}", tags=["Lecturer: Photo"], deprecated=True)
router = APIRouter(prefix="/lecturer/{lecturer_id}", tags=["Lecturer: Photo"])
Expand All @@ -28,7 +30,7 @@ async def upload_photo(lecturer_id: int, photo: UploadFile = File(...)) -> Photo
requests.post(url=f'{root}/timetable/lecturer/{lecturer_id}/photo', files={"photo": data})
```
"""
photo = await utils.upload_lecturer_photo(lecturer_id, db.session, file=photo)
photo = await upload_lecturer_photo(lecturer_id, db.session, file=photo)
db.session.commit()
return Photo.from_orm(photo)

Expand All @@ -43,7 +45,12 @@ async def get_lecturer_photos(lecturer_id: int, limit: int = 10, offset: int = 0
cnt, res = res.count(), res.offset(offset).limit(limit).all()
else:
cnt, res = res.count(), res.offset(offset).all()
return LecturerPhotos(**{"items": [row.link for row in res], "limit": limit, "offset": offset, "total": cnt})
return LecturerPhotos(
items=[get_photo_webpath(row.link) for row in res],
limit=limit,
offset=offset,
total=cnt,
)


@lecturer_photo_router.delete("/photo/{id}", response_model=None) # DEPRICATED TODO: Drop 2023-04-01
Expand Down
Loading