diff --git a/rating_api/models/db.py b/rating_api/models/db.py index b95ff10..61b6c63 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -5,6 +5,7 @@ import uuid from enum import Enum +from fastapi_sqlalchemy import db from sqlalchemy import UUID, Boolean, DateTime from sqlalchemy import Enum as DbEnum from sqlalchemy import ForeignKey, Integer, String, UnaryExpression, and_, func, nulls_last, or_, true @@ -12,12 +13,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.attributes import InstrumentedAttribute -from rating_api.settings import get_settings +from rating_api.utils.mark import calc_weighted_mark from .base import BaseDbModel -settings = get_settings() logger = logging.getLogger(__name__) @@ -62,16 +62,41 @@ def search_by_subject(self, query: str) -> bool: return response @hybrid_method - def order_by_mark(self, query: str, asc_order: bool) -> UnaryExpression[float]: - return ( - nulls_last(func.avg(getattr(Comment, query))) - if asc_order - else nulls_last(func.avg(getattr(Comment, query)).desc()) - ) + def order_by_mark( + self, query: str, asc_order: bool + ) -> tuple[UnaryExpression[float], InstrumentedAttribute, InstrumentedAttribute]: + if "mark_weighted" in query: + comments_num = func.count(self.comments).filter(Comment.review_status == ReviewStatus.APPROVED) + lecturer_mark_general = func.avg(Comment.mark_general).filter( + Comment.review_status == ReviewStatus.APPROVED + ) + expression = calc_weighted_mark(lecturer_mark_general, comments_num, Lecturer.mean_mark_general()) + else: + expression = func.avg(getattr(Comment, query)).filter(Comment.review_status == ReviewStatus.APPROVED) + if not asc_order: + expression = expression.desc() + return nulls_last(expression), Lecturer.last_name, Lecturer.id @hybrid_method - def order_by_name(self, query: str, asc_order: bool) -> UnaryExpression[str] | InstrumentedAttribute[str]: - return getattr(Lecturer, query) if asc_order else getattr(Lecturer, query).desc() + def order_by_name( + self, query: str, asc_order: bool + ) -> tuple[UnaryExpression[str] | InstrumentedAttribute, InstrumentedAttribute]: + return (getattr(Lecturer, query) if asc_order else getattr(Lecturer, query).desc()), Lecturer.id + + @staticmethod + def mean_mark_general() -> float: + mark_general_rows = ( + db.session.query(func.avg(Comment.mark_general)) + .filter(Comment.review_status == ReviewStatus.APPROVED) + .group_by(Comment.lecturer_id) + .all() + ) + mean_mark_general = float( + sum(mark_general_row[0] for mark_general_row in mark_general_rows) / len(mark_general_rows) + if len(mark_general_rows) != 0 + else 0 + ) + return mean_mark_general class Comment(BaseDbModel): diff --git a/rating_api/routes/lecturer.py b/rating_api/routes/lecturer.py index e7c0e4d..64632fa 100644 --- a/rating_api/routes/lecturer.py +++ b/rating_api/routes/lecturer.py @@ -9,6 +9,7 @@ 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.utils.mark import calc_weighted_mark lecturer = APIRouter(prefix="/lecturer", tags=["Lecturer"]) @@ -65,6 +66,9 @@ async def get_lecturer(id: int, info: list[Literal["comments", "mark"]] = Query( result.mark_kindness = sum(comment.mark_kindness for comment in approved_comments) / len(approved_comments) result.mark_clarity = sum(comment.mark_clarity for comment in approved_comments) / len(approved_comments) result.mark_general = sum(comment.mark_general for comment in approved_comments) / len(approved_comments) + result.mark_weighted = calc_weighted_mark( + result.mark_general, len(approved_comments), Lecturer.mean_mark_general() + ) if approved_comments: result.subjects = list({comment.subject for comment in approved_comments}) return result @@ -76,7 +80,8 @@ async def get_lecturers( offset: int = 0, info: list[Literal["comments", "mark"]] = Query(default=[]), order_by: str = Query( - enum=["mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"], default="mark_general" + enum=["mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"], + default="mark_weighted", ), subject: str = Query(''), name: str = Query(''), @@ -87,7 +92,7 @@ async def get_lecturers( `offset` - нижняя граница получения преподавателей, т.е. если по дефолту первым возвращается преподаватель с условным номером N, то при наличии ненулевого offset будет возвращаться преподаватель с номером N + offset - `order_by` - возможные значения `"mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"`. + `order_by` - возможные значения `"mark_weighted", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "last_name"`. Если передано `'last_name'` - возвращается список преподавателей отсортированных по алфавиту по фамилиям Если передано `'mark_...'` - возвращается список преподавателей отсортированных по конкретной оценке @@ -113,9 +118,11 @@ async def get_lecturers( .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) + *( + Lecturer.order_by_mark(order_by, asc_order) + if "mark" in order_by + else Lecturer.order_by_name(order_by, asc_order) + ) ) ) @@ -125,6 +132,8 @@ async def get_lecturers( 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() for db_lecturer in lecturers: lecturer_to_result: LecturerGet = LecturerGet.model_validate(db_lecturer) lecturer_to_result.comments = None @@ -151,6 +160,9 @@ async def get_lecturers( lecturer_to_result.mark_general = sum(comment.mark_general for comment in approved_comments) / len( approved_comments ) + lecturer_to_result.mark_weighted = calc_weighted_mark( + lecturer_to_result.mark_general, len(approved_comments), mean_mark_general + ) if approved_comments: lecturer_to_result.subjects = list({comment.subject for comment in approved_comments}) result.lecturers.append(lecturer_to_result) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 4cfa074..6b84bce 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -72,6 +72,7 @@ class LecturerGet(Base): mark_freebie: float | None = None mark_clarity: float | None = None mark_general: float | None = None + mark_weighted: float | None = None comments: list[CommentGet] | None = None diff --git a/rating_api/settings.py b/rating_api/settings.py index a67692b..3d23d67 100644 --- a/rating_api/settings.py +++ b/rating_api/settings.py @@ -21,6 +21,7 @@ class Settings(BaseSettings): COMMENT_LECTURER_FREQUENCE_IN_MONTH: int = 6 COMMENT_LIMIT: int = 20 COMMENT_TO_LECTURER_LIMIT: int = 5 + MEAN_MARK_GENERAL_WEIGHT: float = 0.5 CORS_ALLOW_ORIGINS: list[str] = ['*'] CORS_ALLOW_CREDENTIALS: bool = True CORS_ALLOW_METHODS: list[str] = ['*'] diff --git a/rating_api/utils/mark.py b/rating_api/utils/mark.py new file mode 100644 index 0000000..bb8de9b --- /dev/null +++ b/rating_api/utils/mark.py @@ -0,0 +1,20 @@ +from typing import Any + +from sqlalchemy import ColumnExpressionArgument, UnaryExpression + +from rating_api.settings import get_settings + + +settings = get_settings() + + +def calc_weighted_mark( + lecturer_mark_general: float | ColumnExpressionArgument[float], + lecturer_comments_num: int | ColumnExpressionArgument[int], + mean_mark_general: float, +) -> float | UnaryExpression[Any]: + total_weight = lecturer_comments_num + settings.MEAN_MARK_GENERAL_WEIGHT + mark_weighted = ( + lecturer_mark_general * lecturer_comments_num + mean_mark_general * settings.MEAN_MARK_GENERAL_WEIGHT + ) / total_weight + return mark_weighted