модельки для проекта Лены

This commit is contained in:
2025-06-10 14:44:25 +04:00
parent 57ebddee03
commit 1ce37804c8
18 changed files with 228 additions and 92 deletions

View File

@@ -9,3 +9,9 @@ remove:
dev: dev:
uvicorn app.main:app --reload uvicorn app.main:app --reload
makemigration:
alembic revision --autogenerate
migrate:
alembic upgrade head

View File

@@ -1,6 +1,26 @@
from app.dao.base import BaseDAO from app.dao.base import BaseDAO
from app.auth.models import User from app.models import *
class UsersDAO(BaseDAO): class DeansDAO(BaseDAO):
model = User model = Dean
class FacultsDAO(BaseDAO):
model = Facult
class StudentsDAO(BaseDAO):
model = Student
class SpecializationsDAO(BaseDAO):
model = Specialization
class DeansDAO(BaseDAO):
model = Dean
class GroupsDAO(BaseDAO):
model = Group

View File

@@ -1,2 +0,0 @@
# flake8: noqa
from .user import User

View File

@@ -1,13 +0,0 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from app.dao.database import Base
class User(Base):
first_name: Mapped[str] = mapped_column(String(50))
last_name: Mapped[str] = mapped_column(String(50))
email: Mapped[str] = mapped_column(String(50), unique=True)
password: Mapped[str] = mapped_column(String(256))
def __repr__(self):
return f"{self.__class__.__name__}(id={self.id})"

View File

@@ -2,7 +2,7 @@ from typing import List
from fastapi import APIRouter, Response, Depends from fastapi import APIRouter, Response, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.models import User from app.models import Dean
from app.auth.utils import authenticate_user, set_tokens from app.auth.utils import authenticate_user, set_tokens
from app.dependencies.auth_dep import ( from app.dependencies.auth_dep import (
get_current_user, get_current_user,
@@ -10,18 +10,18 @@ from app.dependencies.auth_dep import (
) )
from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit
from app.exceptions import UserAlreadyExistsException, IncorrectEmailOrPasswordException from app.exceptions import UserAlreadyExistsException, IncorrectEmailOrPasswordException
from app.auth.dao import UsersDAO from app.auth.dao import DeansDAO
from app.auth.schemas import SUserRegister, SUserAuth, EmailModel, SUserAddDB, SUserInfo from app.auth.schemas import SDeanRegister, SDeanAuth, EmailModel, SDeanAddDB, SDeanInfo
router = APIRouter() router = APIRouter()
@router.post("/register/") @router.post("/register/")
async def register_user( async def register_user(
user_data: SUserRegister, session: AsyncSession = Depends(get_session_with_commit) user_data: SDeanRegister, session: AsyncSession = Depends(get_session_with_commit)
) -> dict: ) -> dict:
# Проверка существования пользователя # Проверка существования пользователя
user_dao = UsersDAO(session) user_dao = DeansDAO(session)
existing_user = await user_dao.find_one_or_none( existing_user = await user_dao.find_one_or_none(
filters=EmailModel(email=user_data.email) filters=EmailModel(email=user_data.email)
@@ -34,7 +34,7 @@ async def register_user(
user_data_dict.pop("confirm_password", None) user_data_dict.pop("confirm_password", None)
# Добавление пользователя # Добавление пользователя
await user_dao.add(values=SUserAddDB(**user_data_dict)) await user_dao.add(values=SDeanAddDB(**user_data_dict))
return {"message": "Вы успешно зарегистрированы!"} return {"message": "Вы успешно зарегистрированы!"}
@@ -42,10 +42,10 @@ async def register_user(
@router.post("/login/") @router.post("/login/")
async def auth_user( async def auth_user(
response: Response, response: Response,
user_data: SUserAuth, user_data: SDeanAuth,
session: AsyncSession = Depends(get_session_without_commit), session: AsyncSession = Depends(get_session_without_commit),
) -> dict: ) -> dict:
users_dao = UsersDAO(session) users_dao = DeansDAO(session)
user = await users_dao.find_one_or_none(filters=EmailModel(email=user_data.email)) user = await users_dao.find_one_or_none(filters=EmailModel(email=user_data.email))
if not (user and await authenticate_user(user=user, password=user_data.password)): if not (user and await authenticate_user(user=user, password=user_data.password)):
@@ -62,13 +62,13 @@ async def logout(response: Response):
@router.get("/me/") @router.get("/me/")
async def get_me(user_data: User = Depends(get_current_user)) -> SUserInfo: async def get_me(user_data: Dean = Depends(get_current_user)) -> SDeanInfo:
return SUserInfo.model_validate(user_data) return SDeanInfo.model_validate(user_data)
@router.post("/refresh") @router.post("/refresh")
async def process_refresh_token( async def process_refresh_token(
response: Response, user: User = Depends(check_refresh_token) response: Response, user: Dean = Depends(check_refresh_token)
): ):
set_tokens(response, user.id) set_tokens(response, user.id)
return {"message": "Токены успешно обновлены"} return {"message": "Токены успешно обновлены"}

View File

@@ -15,15 +15,21 @@ class EmailModel(BaseModel):
class UserBase(EmailModel): class UserBase(EmailModel):
first_name: str = Field( name: str = Field(
min_length=3, max_length=50, description="Имя, от 3 до 50 символов" min_length=3, max_length=50, description="Имя, от 3 до 50 символов"
) )
last_name: str = Field( surname: str = Field(
min_length=3, max_length=50, description="Фамилия, от 3 до 50 символов" min_length=3, max_length=50, description="Фамилия, от 3 до 50 символов"
) )
patronymic: str = Field(
min_length=3,
max_length=50,
description="Отчество, от 3 до 50 символов",
default=None,
)
class SUserRegister(UserBase): class SDeanRegister(UserBase):
password: str = Field( password: str = Field(
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков" min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
) )
@@ -41,15 +47,15 @@ class SUserRegister(UserBase):
return self return self
class SUserAddDB(UserBase): class SDeanAddDB(UserBase):
password: str = Field(min_length=5, description="Пароль в формате HASH-строки") password: str = Field(min_length=5, description="Пароль в формате HASH-строки")
class SUserAuth(EmailModel): class SDeanAuth(EmailModel):
password: str = Field( password: str = Field(
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков" min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
) )
class SUserInfo(UserBase): class SDeanInfo(UserBase):
id: int = Field(description="Идентификатор пользователя") id: int = Field(description="Идентификатор пользователя")

View File

@@ -2,7 +2,7 @@ import uuid
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from sqlalchemy import func, TIMESTAMP, Integer, inspect from sqlalchemy import Integer, inspect
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
AsyncAttrs, AsyncAttrs,
@@ -24,10 +24,6 @@ class Base(AsyncAttrs, DeclarativeBase):
__abstract__ = True __abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
TIMESTAMP, server_default=func.now(), onupdate=func.now()
)
@declared_attr @declared_attr
def __tablename__(cls) -> str: def __tablename__(cls) -> str:
@@ -63,6 +59,4 @@ class Base(AsyncAttrs, DeclarativeBase):
def __repr__(self) -> str: def __repr__(self) -> str:
"""Строковое представление объекта для удобства отладки.""" """Строковое представление объекта для удобства отладки."""
return f"<{self.__class__.__name__}(id={self.id}, " return f"<{self.__class__.__name__}(id={self.id})>"
"created_at={self.created_at}, updated_at={self.updated_at})>"

View File

@@ -3,8 +3,8 @@ from fastapi import Request, Depends
from jose import jwt, JWTError, ExpiredSignatureError from jose import jwt, JWTError, ExpiredSignatureError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.dao import UsersDAO from app.auth.dao import DeansDAO
from app.auth.models import User from app.models import Dean
from app.config import settings from app.config import settings
from app.dependencies.dao_dep import get_session_without_commit from app.dependencies.dao_dep import get_session_without_commit
from app.exceptions import ( from app.exceptions import (
@@ -36,7 +36,7 @@ def get_refresh_token(request: Request) -> str:
async def check_refresh_token( async def check_refresh_token(
token: str = Depends(get_refresh_token), token: str = Depends(get_refresh_token),
session: AsyncSession = Depends(get_session_without_commit), session: AsyncSession = Depends(get_session_without_commit),
) -> User: ) -> Dean:
"""Проверяем refresh_token и возвращаем пользователя.""" """Проверяем refresh_token и возвращаем пользователя."""
try: try:
payload = jwt.decode( payload = jwt.decode(
@@ -46,7 +46,7 @@ async def check_refresh_token(
if not user_id: if not user_id:
raise NoJwtException raise NoJwtException
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id)) user = await DeansDAO(session).find_one_or_none_by_id(data_id=int(user_id))
if not user: if not user:
raise NoJwtException raise NoJwtException
@@ -58,7 +58,7 @@ async def check_refresh_token(
async def get_current_user( async def get_current_user(
token: str = Depends(get_access_token), token: str = Depends(get_access_token),
session: AsyncSession = Depends(get_session_without_commit), session: AsyncSession = Depends(get_session_without_commit),
) -> User: ) -> Dean:
"""Проверяем access_token и возвращаем пользователя.""" """Проверяем access_token и возвращаем пользователя."""
try: try:
# Декодируем токен # Декодируем токен
@@ -80,15 +80,15 @@ async def get_current_user(
if not user_id: if not user_id:
raise NoUserIdException raise NoUserIdException
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id)) user = await DeansDAO(session).find_one_or_none_by_id(data_id=int(user_id))
if not user: if not user:
raise UserNotFoundException raise UserNotFoundException
return user return user
async def get_current_admin_user( async def get_current_admin_user(
current_user: User = Depends(get_current_user), current_user: Dean = Depends(get_current_user),
) -> User: ) -> Dean:
"""Проверяем права пользователя как администратора.""" """Проверяем права пользователя как администратора."""
if current_user.role.id in [3, 4]: if current_user.role.id in [3, 4]:
return current_user return current_user

View File

@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context from alembic import context
from app.config import database_url from app.config import database_url
from app.dao.database import Base from app.dao.database import Base
from app.auth.models import * from app.models import *
config = context.config config = context.config
config.set_main_option("sqlalchemy.url", database_url) config.set_main_option("sqlalchemy.url", database_url)

View File

@@ -0,0 +1,78 @@
"""empty message
Revision ID: cae942c398cb
Revises:
Create Date: 2025-06-10 14:41:29.391395
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'cae942c398cb'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('deans',
sa.Column('email', sa.String(length=50), nullable=False),
sa.Column('password', sa.String(length=256), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.Column('surname', sa.String(length=50), nullable=False),
sa.Column('patronymic', sa.String(length=50), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('specializations',
sa.Column('name', sa.String(length=256), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code'),
sa.UniqueConstraint('name')
)
op.create_table('facults',
sa.Column('name', sa.String(length=256), nullable=False),
sa.Column('dean_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.ForeignKeyConstraint(['dean_id'], ['deans.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('groups',
sa.Column('course', sa.Integer(), nullable=False),
sa.Column('specialization_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.ForeignKeyConstraint(['specialization_id'], ['specializations.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('students',
sa.Column('specialization_id', sa.Integer(), nullable=False),
sa.Column('status', sa.Enum('STUDY', 'ACADEM', 'EXPLUSION', 'GRADUATED', name='studentstatus'), nullable=False),
sa.Column('group_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.Column('surname', sa.String(length=50), nullable=False),
sa.Column('patronymic', sa.String(length=50), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
sa.ForeignKeyConstraint(['specialization_id'], ['specializations.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('students')
op.drop_table('groups')
op.drop_table('facults')
op.drop_table('specializations')
op.drop_table('deans')
# ### end Alembic commands ###

View File

@@ -1,40 +0,0 @@
"""Initial
Revision ID: faa64c3fcd68
Revises:
Create Date: 2025-06-09 22:57:38.958126
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'faa64c3fcd68'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('first_name', sa.String(length=50), nullable=False),
sa.Column('last_name', sa.String(length=50), nullable=False),
sa.Column('email', sa.String(length=50), nullable=False),
sa.Column('password', sa.String(length=256), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users')
# ### end Alembic commands ###

View File

@@ -0,0 +1,5 @@
from .student import Student
from .group import Group
from .specialization import Specialization
from .dean import Dean
from .facult import Facult

View File

@@ -0,0 +1,13 @@
from sqlalchemy.orm import Mapped, relationship, mapped_column
from sqlalchemy import String
from app.dao.database import Base
from app.models.facult import Facult
from .mixins import HumanMixin
class Dean(HumanMixin, Base):
facult: Mapped[Facult | None] = relationship(back_populates="dean")
email: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(256), nullable=False)

View File

@@ -0,0 +1,10 @@
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.dao.database import Base
class Facult(Base):
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
dean_id: Mapped[int | None] = mapped_column(ForeignKey("deans.id"), nullable=True)
dean: Mapped["Dean"] = relationship("Dean", back_populates="facult", lazy="joined")

View File

@@ -0,0 +1,12 @@
from sqlalchemy import ForeignKey, Integer
from app.dao.database import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
class Group(Base):
course: Mapped[int] = mapped_column(Integer, nullable=False)
students: Mapped[list["Student"]] = relationship(back_populates="group")
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"))
specialization: Mapped["Specialization"] = relationship(
"Specialization", back_populates="groups", lazy="joined"
)

View File

@@ -0,0 +1,10 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
class HumanMixin:
name: Mapped[str] = mapped_column(String(50), nullable=False)
surname: Mapped[str] = mapped_column(String(50), nullable=False)
patronymic: Mapped[str | None] = mapped_column(
String(50), nullable=True, default=None
)

View File

@@ -0,0 +1,10 @@
from sqlalchemy import String
from app.dao.database import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
class Specialization(Base):
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
students: Mapped[list["Student"]] = relationship(back_populates="specialization")
groups: Mapped[list["Group"]] = relationship(back_populates="specialization")

View File

@@ -0,0 +1,27 @@
import enum
from sqlalchemy import ForeignKey, Enum
from app.dao.database import Base
from .mixins import HumanMixin
from sqlalchemy.orm import Mapped, mapped_column, relationship
class StudentStatus(enum.Enum):
STUDY = "Обучается"
ACADEM = "Академ"
EXPLUSION = "Отчислен"
GRADUATED = "Выпущен"
class Student(HumanMixin, Base):
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"))
specialization: Mapped["Specialization"] = relationship(
"Specialization", back_populates="groups", lazy="joined"
)
status: Mapped[StudentStatus] = mapped_column(
Enum(StudentStatus), nullable=False, default=StudentStatus.STUDY
)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id"))
group: Mapped["Group"] = relationship(
"Group", back_populates="students", lazy="joined"
)