diff --git a/migrations/versions/0fbda260a023_add_user_id.py b/migrations/versions/0fbda260a023_add_user_id.py index fda0582..f5ec0c2 100644 --- a/migrations/versions/0fbda260a023_add_user_id.py +++ b/migrations/versions/0fbda260a023_add_user_id.py @@ -10,7 +10,6 @@ from alembic import op -# revision identifiers, used by Alembic. revision = '0fbda260a023' down_revision = '5659e13277b6' branch_labels = None diff --git a/migrations/versions/16fe9919c686_lecturer_rating.py b/migrations/versions/16fe9919c686_lecturer_rating.py new file mode 100644 index 0000000..7959e9b --- /dev/null +++ b/migrations/versions/16fe9919c686_lecturer_rating.py @@ -0,0 +1,42 @@ +"""lecturer-rating + +Revision ID: 16fe9919c686 +Revises: fc7cb93684e0 +Create Date: 2025-08-24 00:25:32.995215 + +""" + +import sqlalchemy as sa +from alembic import op + + +revision = '16fe9919c686' +down_revision = 'fc7cb93684e0' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'lecturer_rating', + sa.Column('id', sa.Integer(), nullable=False, comment='Идентификатор препода'), + sa.Column( + 'mark_weighted', sa.Float(), nullable=True, comment='Взвешенная оценка преподавателя, посчитана в dwh' + ), + sa.Column( + 'mark_kindness_weighted', sa.Float(), nullable=True, comment='Взвешенная оценка доброты, посчитана в dwh' + ), + sa.Column( + 'mark_clarity_weighted', sa.Float(), nullable=True, comment='Взвешенная оценка понятности, посчитана в dwh' + ), + sa.Column( + 'mark_freebie_weighted', sa.Float(), nullable=True, comment='Взвешенная оценка халявности, посчитана в dwh' + ), + sa.Column('rank', sa.Integer(), nullable=True, comment='Место в рейтинге, посчитана в dwh'), + sa.Column('update_ts', sa.DateTime(), nullable=True, comment='Время обновления записи'), + sa.PrimaryKeyConstraint('id'), + ) + + +def downgrade(): + op.drop_table('lecturer_rating') diff --git a/migrations/versions/20181e0d6aab_make_nullable_timetable_id.py b/migrations/versions/20181e0d6aab_make_nullable_timetable_id.py index efdfd17..4178c35 100644 --- a/migrations/versions/20181e0d6aab_make_nullable_timetable_id.py +++ b/migrations/versions/20181e0d6aab_make_nullable_timetable_id.py @@ -9,7 +9,6 @@ from alembic import op -# revision identifiers, used by Alembic. revision = '20181e0d6aab' down_revision = 'edcc1a448ffb' branch_labels = None diff --git a/migrations/versions/5cf69f1026d9_fixing_comments_pk.py b/migrations/versions/5cf69f1026d9_fixing_comments_pk.py index bbd16da..015f262 100644 --- a/migrations/versions/5cf69f1026d9_fixing_comments_pk.py +++ b/migrations/versions/5cf69f1026d9_fixing_comments_pk.py @@ -9,7 +9,6 @@ from alembic import op -# revision identifiers, used by Alembic. revision = '5cf69f1026d9' down_revision = '933db669e7ef' branch_labels = None diff --git a/migrations/versions/933db669e7ef_make_subject_in_comment_nullable.py b/migrations/versions/933db669e7ef_make_subject_in_comment_nullable.py index 924a186..566a988 100644 --- a/migrations/versions/933db669e7ef_make_subject_in_comment_nullable.py +++ b/migrations/versions/933db669e7ef_make_subject_in_comment_nullable.py @@ -10,7 +10,6 @@ from alembic import op -# revision identifiers, used by Alembic. revision = '933db669e7ef' down_revision = '20181e0d6aab' branch_labels = None diff --git a/migrations/versions/fc7cb93684e0_likes.py b/migrations/versions/fc7cb93684e0_likes.py index 224829e..c6dca55 100644 --- a/migrations/versions/fc7cb93684e0_likes.py +++ b/migrations/versions/fc7cb93684e0_likes.py @@ -10,7 +10,6 @@ from alembic import op -# revision identifiers, used by Alembic. revision = 'fc7cb93684e0' down_revision = '1c001709fc55' branch_labels = None @@ -18,7 +17,6 @@ def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.create_table( 'comment_reaction', sa.Column('uuid', sa.UUID(), nullable=False), @@ -33,10 +31,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint('uuid'), ) - # ### end Alembic commands ### def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.drop_table('comment_reaction') - # ### end Alembic commands ### diff --git a/rating_api/exceptions.py b/rating_api/exceptions.py index 2ac6ca7..e1b0be7 100644 --- a/rating_api/exceptions.py +++ b/rating_api/exceptions.py @@ -87,3 +87,11 @@ def __init__(self, msg: str): f"{msg} Conflict with update a resource that already exists or has conflicting information.", f"{msg} Конфликт с обновлением ресурса, который уже существует или имеет противоречивую информацию.", ) + + +class ValidObjectNotFound(RatingAPIError): + def __init__(self, obj: type, obj_id_or_name: int | str): + super().__init__( + f"Object {obj.__name__} {obj_id_or_name=} not found valid", + f"Объект {obj.__name__} с идентификатором {obj_id_or_name} не найден валидным", + ) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 6ad4a52..f244434 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -13,6 +13,7 @@ ) from sqlalchemy import Enum as DbEnum from sqlalchemy import ( + Float, ForeignKey, Integer, String, @@ -211,3 +212,21 @@ class CommentReaction(BaseDbModel): edited_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.now(datetime.timezone.utc)) user_id: Mapped[int] = mapped_column(Integer, nullable=False) comment = relationship("Comment", back_populates="reactions") + + +class LecturerRating(BaseDbModel): + id: Mapped[int] = mapped_column(Integer, primary_key=True, comment="Идентификатор препода") + mark_weighted: Mapped[float] = mapped_column( + Float, nullable=True, comment="Взвешенная оценка преподавателя, посчитана в dwh" + ) + mark_kindness_weighted: Mapped[float] = mapped_column( + Float, nullable=True, comment="Взвешенная оценка доброты, посчитана в dwh" + ) + mark_clarity_weighted: Mapped[float] = mapped_column( + Float, nullable=True, comment="Взвешенная оценка понятности, посчитана в dwh" + ) + mark_freebie_weighted: Mapped[float] = mapped_column( + Float, nullable=True, comment="Взвешенная оценка халявности, посчитана в dwh" + ) + rank: Mapped[int] = mapped_column(Integer, nullable=True, comment="Место в рейтинге, посчитана в dwh") + update_ts: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=True, comment="Время обновления записи") diff --git a/rating_api/models/dwh.py b/rating_api/models/dwh.py new file mode 100644 index 0000000..a1bf175 --- /dev/null +++ b/rating_api/models/dwh.py @@ -0,0 +1,34 @@ +from datetime import date, datetime +from uuid import UUID + +from models import DWHBaseDbModel +from sqlalchemy.orm import Mapped, mapped_column + + +class DWHLecturer(DWHBaseDbModel): + __tablename__ = 'lecturer' + __tableargs__ = {'schema': "DWH_RATING"} + + uuid: Mapped[UUID] = mapped_column(primary_key=True, comment="Техническое поле в dwh") + api_id: Mapped[int] = mapped_column(comment="Идентифиактор в rating-api") + first_name: Mapped[str] = mapped_column(comment="Имя преподавателя") + last_name: Mapped[str] = mapped_column(comment="Фамилия преподавателя") + middle_name: Mapped[str] = mapped_column(comment="отчество преподавателя") + subject: Mapped[str | None] = mapped_column(comment="Список предметов преподавателя") + avatar_link: Mapped[str | None] = mapped_column(comment="Ссылка на аватар преподавателя") + timetable_id: Mapped[int] = mapped_column(comment="Идертификатор в timetable-api") + rank: Mapped[int] = mapped_column(comment="Место в рейтинге", default=0, server_default="0") + mark_weighted: Mapped[float] = mapped_column( + nullable=False, comment="Взвешенная оценка преподавателя", default=0, server_default="0" + ) + mark_kindness_weighted: Mapped[float] = mapped_column( + nullable=False, comment="Взвешенная доброта преподавателя", default=0, server_default="0" + ) + mark_clarity_weighted: Mapped[float] = mapped_column( + nullable=False, comment="Взверешенная понятность преподавателя", default=0, server_default="0" + ) + mark_freebie_weighted: Mapped[float] = mapped_column( + nullable=False, comment="Взвешенная халявность преподавателя", default=0, server_default="0" + ) + valid_from_dt: Mapped[date | None] = mapped_column(comment="Дата начала действия записи") + valid_to_dt: Mapped[date | None] = mapped_column(comment="Дата конца действия записи") diff --git a/rating_api/models/dwh_base.py b/rating_api/models/dwh_base.py new file mode 100644 index 0000000..dcb86b1 --- /dev/null +++ b/rating_api/models/dwh_base.py @@ -0,0 +1,56 @@ +import re + +from sqlalchemy.exc import NoResultFound +from sqlalchemy.orm import Query, Session, as_declarative, declared_attr + +from rating_api.exceptions import ObjectNotFound, ValidObjectNotFound + + +as_declarative() + + +class DWHBase: + """Base class for all dwh entities""" + + @declared_attr + def __tablename__(cls) -> str: # pylint: disable=no-self-argument + """Generate database table name automatically. + Convert CamelCase class name to snake_case db table name. + """ + return re.sub(r"(? DWHBaseDbModel: + """Get valid object""" + + objs = session.query(cls) + try: + if hasattr(cls, "valid_to_dt"): + objs = objs.filter(cls.valid_to_dt.is_(None)) + except NoResultFound: + raise ValidObjectNotFound(cls, id) + try: + if hasattr(cls, "api_id"): + return objs.filter(cls.api_id == id).one() + except NoResultFound: + raise ObjectNotFound(cls, id) + + @classmethod + def get_all(cls, *, session: Session) -> [DWHBaseDbModel]: + "Get all valid objects" + objs = session.query(cls) + try: + if hasattr(cls, "valid_to_dt"): + objs = objs.filter(cls.valid_to_dt.is_(None)) + except NoResultFound: + raise ValidObjectNotFound(cls, id) diff --git a/rating_api/routes/lecturer.py b/rating_api/routes/lecturer.py index 64632fa..b768fb5 100644 --- a/rating_api/routes/lecturer.py +++ b/rating_api/routes/lecturer.py @@ -8,7 +8,13 @@ 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, +) from rating_api.utils.mark import calc_weighted_mark diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index da53351..d656b5c 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -177,3 +177,30 @@ class LecturerPatch(Base): middle_name: str | None = None avatar_link: str | None = None timetable_id: int | None = None + + +class LecturerRankRatingApi(Base): + id: int | None = None + mark_weighted: float | None = None + mark_kindness_weighted: float | None = None + mark_clarity_weighted: float | None = None + mark_freebie_weighted: float | None = None + rank: float | None = None + + +class LecturerRankDWH(Base): + uuid: UUID | None = None + id: int + first_name: str | None = None + last_name: str | None = None + middle_name: str | None = None + subject: str | None = None + avatar_link: str | None = None + timetable_id: int | None = None + valid_from_dt: datetime.datetime + valid_to_dt: datetime.datetime + rank: int + mark_weighted: float + mark_kindness_weighted: float + mark_clarity_weighted: float + mark_freebie_weighted: float diff --git a/rating_api/services/dwh.py b/rating_api/services/dwh.py new file mode 100644 index 0000000..4e1ab9e --- /dev/null +++ b/rating_api/services/dwh.py @@ -0,0 +1,32 @@ +import datetime + +from models.db import LecturerRating +from models.dwh import DWHLecturer +from settings import get_settings +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from rating_api.exceptions import ObjectNotFound + + +settings = get_settings() + +engine = create_engine(settings.DWH_DB_DSN) + + +def copy_from_dwh(api_session): + with Session(engine) as dwh_session: + lecturers = DWHLecturer.get_all(session=dwh_session) + for lecturer in lecturers: + fields = { + "id": lecturer.api_id, + "mark_weighted": lecturer.mark_weighted, + "mark_kindness_weighted": lecturer.mark_kindness_weighted, + "mark_clarity_weighted": lecturer.mark_clarity_weighted, + "mark_freebie_weighted": lecturer.mark_freebie_weighted, + "rank": lecturer.rank, + "update_ts": datetime.datetime.now(datetime.timezone.utc), + } + + if LecturerRating.get(fields["id"], session=api_session): + LecturerRating.update(fields["id"], session=api_session, **fields) diff --git a/rating_api/settings.py b/rating_api/settings.py index 63069d4..6856333 100644 --- a/rating_api/settings.py +++ b/rating_api/settings.py @@ -9,6 +9,7 @@ class Settings(BaseSettings): """Application settings""" DB_DSN: PostgresDsn = 'postgresql://postgres@localhost:5432/postgres' + DWH_DB_DSN: PostgresDsn = 'postgresql://postgres@localhost:5432/postgres' ROOT_PATH: str = '/' + os.getenv("APP_NAME", "") SERVICE_ID: int = os.getenv("SERVICE_ID", -3) # Указать какой id сервиса COMMENT_FREQUENCY_IN_MONTH: int = 10