Skip to content
Closed
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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ format:
autoflake -r --in-place --remove-all-unused-imports ./migrations
isort ./migrations
black ./migrations
autoflake -r --in-place --remove-all-unused-imports ./tests
isort ./tests
black ./tests

db:
docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-rating_api postgres:15
Expand Down
2 changes: 1 addition & 1 deletion rating_api/routes/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async def create_comment(lecturer_id: int, comment_info: CommentPost, user=Depen
else:
give_achievement = False
if give_achievement:
session.post(
await session.post(
settings.API_URL
+ f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{user.get('id'):}",
headers={"Accept": "application/json", "Authorization": settings.ACHIEVEMENT_GIVE_TOKEN},
Expand Down
59 changes: 32 additions & 27 deletions rating_api/routes/lecturer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

from auth_lib.fastapi import UnionAuth
from fastapi import APIRouter, Depends, Query
from fastapi_filter import FilterDepends
from fastapi_sqlalchemy import db
from sqlalchemy import and_

from rating_api.exceptions import AlreadyExists, ObjectNotFound
from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus
from rating_api.schemas.base import StatusResponseModel
from rating_api.schemas.models import CommentGet, LecturerGet, LecturerGetAll, LecturerPatch, LecturerPost
from rating_api.schemas.models import (
CommentGet,
LecturerGet,
LecturerGetAll,
LecturerPatch,
LecturerPost,
LecturersFilter,
)
from rating_api.utils.mark import calc_weighted_mark


Expand Down Expand Up @@ -76,16 +84,11 @@ async def get_lecturer(id: int, info: list[Literal["comments", "mark"]] = Query(

@lecturer.get("", response_model=LecturerGetAll)
async def get_lecturers(
lecturer_filter=FilterDepends(LecturersFilter),
limit: int = 10,
offset: int = 0,
info: list[Literal["comments", "mark"]] = Query(default=[]),
order_by: str = Query(
enum=["mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"],
default="mark_weighted",
),
subject: str = Query(''),
name: str = Query(''),
asc_order: bool = False,
mark: float = Query(default=None, ge=-2, le=2),
) -> LecturerGetAll:
"""
`limit` - максимальное количество возвращаемых преподавателей
Expand All @@ -95,6 +98,13 @@ async def get_lecturers(
`order_by` - возможные значения `"mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"`.
Если передано `'last_name'` - возвращается список преподавателей отсортированных по алфавиту по фамилиям
Если передано `'mark_...'` - возвращается список преподавателей отсортированных по конкретной оценке
Если передано просто так (или с '+' в начале параметра), то сортирует по возрастанию
С '-' в начале -- по убыванию.

*Пример запросов с этим параметром*:
- `...?order_by=-mark_kindness`
- `...?order_by=mark_freebie`
- `...?order_by=+mark_freebie` (эквивалентно 2ому пункту)

`info` - возможные значения `'comments'`, `'mark'`.
Если передано `'comments'`, то возвращаются одобренные комментарии к преподавателю.
Expand All @@ -107,30 +117,17 @@ async def get_lecturers(
`name`
Поле для ФИО. Если передано `name` - возвращает всех преподователей, для которых нашлись совпадения с переданной строкой

`asc_order`
Если передано true, сортировать в порядке возрастания
Иначе - в порядке убывания
`mark`
Поле для оценки. Если передано, то возвращает только тех преподавателей, для которых средняя общая оценка ('general_mark')
больше, чем переданный 'mark'.
"""
lecturers_query = (
Lecturer.query(session=db.session)
.outerjoin(Lecturer.comments)
.group_by(Lecturer.id)
.filter(Lecturer.search_by_subject(subject))
.filter(Lecturer.search_by_name(name))
.order_by(
*(
Lecturer.order_by_mark(order_by, asc_order)
if "mark" in order_by
else Lecturer.order_by_name(order_by, asc_order)
)
)
lecturers_query = lecturer_filter.filter(
Lecturer.query(session=db.session).outerjoin(Lecturer.comments).group_by(Lecturer.id)
)

lecturers_query = lecturer_filter.sort(lecturers_query)
lecturers = lecturers_query.offset(offset).limit(limit).all()
lecturers_count = lecturers_query.group_by(Lecturer.id).count()

if not lecturers:
raise ObjectNotFound(Lecturer, 'all')
result = LecturerGetAll(limit=limit, offset=offset, total=lecturers_count)
if "mark" in info:
mean_mark_general = Lecturer.mean_mark_general()
Expand All @@ -143,6 +140,12 @@ async def get_lecturers(
for comment in db_lecturer.comments
if comment.review_status is ReviewStatus.APPROVED
]
if (
mark is not None
and approved_comments
and sum(comment.mark_general for comment in approved_comments) / len(approved_comments) < mark
):
continue
if "comments" in info and approved_comments:
lecturer_to_result.comments = sorted(
approved_comments, key=lambda comment: comment.create_ts, reverse=True
Expand All @@ -166,6 +169,8 @@ async def get_lecturers(
if approved_comments:
lecturer_to_result.subjects = list({comment.subject for comment in approved_comments})
result.lecturers.append(lecturer_to_result)
if len(result.lecturers) == 0:
raise ObjectNotFound(Lecturer, 'all')
return result


Expand Down
141 changes: 71 additions & 70 deletions rating_api/schemas/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import datetime
from typing import List
from uuid import UUID

from pydantic import field_validator
from fastapi import Query
from fastapi_filter.contrib.sqlalchemy import Filter
from pydantic import ValidationInfo, field_validator

from rating_api.exceptions import WrongMark
from rating_api.models import ReviewStatus
from rating_api.models import Lecturer, ReviewStatus
from rating_api.schemas.base import Base


Expand All @@ -24,57 +27,13 @@ class CommentGet(Base):
dislike_count: int


class CommentGetWithStatus(Base):
uuid: UUID
user_id: int | None = None
create_ts: datetime.datetime
update_ts: datetime.datetime
subject: str | None = None
text: str
mark_kindness: int
mark_freebie: int
mark_clarity: int
mark_general: float
lecturer_id: int
class CommentGetWithStatus(CommentGet):
review_status: ReviewStatus
like_count: int
dislike_count: int


class CommentGetWithAllInfo(Base):
uuid: UUID
user_id: int | None = None
create_ts: datetime.datetime
update_ts: datetime.datetime
subject: str | None = None
text: str
mark_kindness: int
mark_freebie: int
mark_clarity: int
mark_general: float
lecturer_id: int
class CommentGetWithAllInfo(CommentGet):
review_status: ReviewStatus
approved_by: int | None = None
like_count: int
dislike_count: int


class CommentPost(Base):
subject: str
text: str
create_ts: datetime.datetime | None = None
update_ts: datetime.datetime | None = None
mark_kindness: int
mark_freebie: int
mark_clarity: int
is_anonymous: bool = True

@field_validator('mark_kindness', 'mark_freebie', 'mark_clarity')
@classmethod
def validate_mark(cls, value):
if value not in [-2, -1, 0, 1, 2]:
raise WrongMark()
return value


class CommentUpdate(Base):
Expand All @@ -92,22 +51,16 @@ def validate_mark(cls, value):
return value


class CommentImport(Base):
lecturer_id: int
subject: str | None = None
text: str
class CommentPost(CommentUpdate):
create_ts: datetime.datetime | None = None
update_ts: datetime.datetime | None = None
mark_kindness: int
mark_freebie: int
mark_clarity: int
is_anonymous: bool = True

@field_validator('mark_kindness', 'mark_freebie', 'mark_clarity')
@classmethod
def validate_mark(cls, value):
if value not in [-2, -1, 0, 1, 2]:
raise WrongMark()
return value

class CommentImport(CommentUpdate):
lecturer_id: int
create_ts: datetime.datetime | None = None
update_ts: datetime.datetime | None = None


class CommentImportAll(Base):
Expand All @@ -123,16 +76,10 @@ class CommentGetAll(Base):

class CommentGetAllWithStatus(Base):
comments: list[CommentGetWithStatus] = []
limit: int
offset: int
total: int


class CommentGetAllWithAllInfo(Base):
comments: list[CommentGetWithAllInfo] = []
limit: int
offset: int
total: int


class LecturerUserCommentPost(Base):
Expand Down Expand Up @@ -171,9 +118,63 @@ class LecturerPost(Base):
timetable_id: int | None = None


class LecturerPatch(Base):
class LecturerPatch(LecturerPost):
first_name: str | None = None
last_name: str | None = None
middle_name: str | None = None
avatar_link: str | None = None
timetable_id: int | None = None


class LecturersFilter(Filter):
subject: str = ''
name: str = ''
order_by: List[str] = [
'mark_weighted',
]

@field_validator("*", mode="before", check_fields=False)
def validate_order_by(cls, value, field: ValidationInfo):
return value

@field_validator('order_by', mode='before')
def check_order_param(cls, value: str) -> str:
"""Проверяет, что значение поля (без +/-) входит в список возможных."""
allowed_ordering = {
"mark_weighted",
"mark_kindness",
"mark_freebie",
"mark_clarity",
"mark_general",
"last_name",
}
cleaned_value = value.replace("+", "").replace("-", "")
if cleaned_value in allowed_ordering:
return value
else:
raise ValueError(f'"order_by"-field must contain value from {allowed_ordering}.')

def filter(self, query: Query) -> Query:
if self.subject:
query = query.filter(self.Constants.model.search_by_subject(self.subject))
if self.name:
query = query.filter(self.Constants.model.search_by_name(self.name))
return query

def sort(self, query: Query) -> Query:
if not self.ordering_values:
return query
elif len(self.ordering_values) > 1:
raise ValueError('order_by (хотя бы пока что) поддерживает лишь один параметр для сортировки!')

for field_name in self.ordering_values:
direction = True
if field_name.startswith("-"):
direction = False
field_name = field_name.replace("-", "").replace("+", "")
if field_name.startswith('mark_'):
query = query.order_by(*self.Constants.model.order_by_mark(field_name, direction))
else:
query = query.order_by(*self.Constants.model.order_by_name(field_name, direction))
return query

class Constants(Filter.Constants):
model = Lecturer
1 change: 1 addition & 0 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ isort
pytest
pytest-cov
pytest-mock
testcontainers[postgres]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ auth-lib-profcomff[fastapi]
aiohttp
fastapi
fastapi-sqlalchemy
fastapi-filter[sqlalchemy]
gunicorn
logging-profcomff
psycopg2-binary
Expand Down
Loading