nspotapov/back (#4)
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
1
backend/app/auth/__init__.py
Normal file
1
backend/app/auth/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .router import router
|
||||
@@ -1,26 +0,0 @@
|
||||
from app.dao.base import BaseDAO
|
||||
from app.models import *
|
||||
|
||||
|
||||
class DeansDAO(BaseDAO):
|
||||
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
|
||||
@@ -1,27 +1,26 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Response, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import Dean
|
||||
from app.auth.utils import authenticate_user, set_tokens
|
||||
from app.dao import UsersDAO
|
||||
from app.dependencies.auth_dep import (
|
||||
get_current_user,
|
||||
check_refresh_token,
|
||||
)
|
||||
from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit
|
||||
from app.exceptions import UserAlreadyExistsException, IncorrectEmailOrPasswordException
|
||||
from app.auth.dao import DeansDAO
|
||||
from app.auth.schemas import SDeanRegister, SDeanAuth, EmailModel, SDeanAddDB, SDeanInfo
|
||||
from app.models import User
|
||||
from app.schemas import SUserRegister, SUserAuth, EmailModel, SUserAddDB, SUserInfo
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register/")
|
||||
@router.post("/register")
|
||||
async def register_user(
|
||||
user_data: SDeanRegister, session: AsyncSession = Depends(get_session_with_commit)
|
||||
user_data: SUserRegister, session: AsyncSession = Depends(get_session_with_commit)
|
||||
) -> dict:
|
||||
# Проверка существования пользователя
|
||||
user_dao = DeansDAO(session)
|
||||
user_dao = UsersDAO(session)
|
||||
|
||||
existing_user = await user_dao.find_one_or_none(
|
||||
filters=EmailModel(email=user_data.email)
|
||||
@@ -34,24 +33,24 @@ async def register_user(
|
||||
user_data_dict.pop("confirm_password", None)
|
||||
|
||||
# Добавление пользователя
|
||||
await user_dao.add(values=SDeanAddDB(**user_data_dict))
|
||||
await user_dao.add(values=SUserAddDB(**user_data_dict))
|
||||
|
||||
return {"message": "Вы успешно зарегистрированы!"}
|
||||
|
||||
|
||||
@router.post("/login/")
|
||||
@router.post("/login")
|
||||
async def auth_user(
|
||||
response: Response,
|
||||
user_data: SDeanAuth,
|
||||
user_data: SUserAuth,
|
||||
session: AsyncSession = Depends(get_session_without_commit),
|
||||
) -> dict:
|
||||
users_dao = DeansDAO(session)
|
||||
users_dao = UsersDAO(session)
|
||||
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)):
|
||||
raise IncorrectEmailOrPasswordException
|
||||
set_tokens(response, user.id)
|
||||
return {"ok": True, "message": "Авторизация успешна!"}
|
||||
return {"message": "Авторизация успешна!"}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
@@ -61,14 +60,14 @@ async def logout(response: Response):
|
||||
return {"message": "Пользователь успешно вышел из системы"}
|
||||
|
||||
|
||||
@router.get("/me/")
|
||||
async def get_me(user_data: Dean = Depends(get_current_user)) -> SDeanInfo:
|
||||
return SDeanInfo.model_validate(user_data)
|
||||
@router.get("/me")
|
||||
async def get_me(user_data: User = Depends(get_current_user)) -> SUserInfo:
|
||||
return SUserInfo.model_validate(user_data)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def process_refresh_token(
|
||||
response: Response, user: Dean = Depends(check_refresh_token)
|
||||
response: Response, user: User = Depends(check_refresh_token)
|
||||
):
|
||||
set_tokens(response, user.id)
|
||||
return {"message": "Токены успешно обновлены"}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import os
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
if os.path.exists(os.path.join(BASE_DIR, ".env")):
|
||||
import dotenv
|
||||
dotenv.load_dotenv(os.path.join(BASE_DIR, ".env"))
|
||||
|
||||
class Settings(BaseSettings):
|
||||
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
BASE_DIR: str = BASE_DIR
|
||||
DB_HOST: str = os.getenv("DB_HOST")
|
||||
DB_PORT: str = os.getenv("DB_PORT")
|
||||
DB_DRIVER: str = os.getenv("DB_DRIVER")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .database import async_session_maker
|
||||
|
||||
from .user import UsersDAO
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from typing import List, TypeVar, Generic, Type
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete, func
|
||||
from loguru import logger
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from .database import Base
|
||||
|
||||
T = TypeVar("T", bound=Base)
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete, func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.models import Model
|
||||
|
||||
T = TypeVar("T", bound=Model)
|
||||
|
||||
|
||||
class BaseDAO(Generic[T]):
|
||||
|
||||
@@ -1,62 +1,13 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
from sqlalchemy import Integer, inspect
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncAttrs,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
AsyncSession,
|
||||
)
|
||||
|
||||
from app.config import database_url
|
||||
|
||||
|
||||
engine = create_async_engine(url=database_url)
|
||||
|
||||
async_session_maker = async_sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
str_uniq = Annotated[str, mapped_column(unique=True, nullable=False)]
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
__abstract__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
return cls.__name__.lower() + "s"
|
||||
|
||||
def to_dict(self, exclude_none: bool = False):
|
||||
"""
|
||||
Преобразует объект модели в словарь.
|
||||
|
||||
Args:
|
||||
exclude_none (bool): Исключать ли None значения из результата
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными объекта
|
||||
"""
|
||||
result = {}
|
||||
for column in inspect(self.__class__).columns:
|
||||
value = getattr(self, column.key)
|
||||
|
||||
# Преобразование специальных типов данных
|
||||
if isinstance(value, datetime):
|
||||
value = value.isoformat()
|
||||
elif isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
elif isinstance(value, uuid.UUID):
|
||||
value = str(value)
|
||||
|
||||
# Добавляем значение в результат
|
||||
if not exclude_none or value is not None:
|
||||
result[column.key] = value
|
||||
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Строковое представление объекта для удобства отладки."""
|
||||
return f"<{self.__class__.__name__}(id={self.id})>"
|
||||
|
||||
6
backend/app/dao/user.py
Normal file
6
backend/app/dao/user.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app.models import User
|
||||
from .base import BaseDAO
|
||||
|
||||
|
||||
class UsersDAO(BaseDAO):
|
||||
model = User
|
||||
@@ -3,8 +3,8 @@ from fastapi import Request, Depends
|
||||
from jose import jwt, JWTError, ExpiredSignatureError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.dao import DeansDAO
|
||||
from app.models import Dean
|
||||
from app.dao import UsersDAO
|
||||
from app.models import User
|
||||
from app.config import settings
|
||||
from app.dependencies.dao_dep import get_session_without_commit
|
||||
from app.exceptions import (
|
||||
@@ -36,7 +36,7 @@ def get_refresh_token(request: Request) -> str:
|
||||
async def check_refresh_token(
|
||||
token: str = Depends(get_refresh_token),
|
||||
session: AsyncSession = Depends(get_session_without_commit),
|
||||
) -> Dean:
|
||||
) -> User:
|
||||
"""Проверяем refresh_token и возвращаем пользователя."""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
@@ -46,7 +46,7 @@ async def check_refresh_token(
|
||||
if not user_id:
|
||||
raise NoJwtException
|
||||
|
||||
user = await DeansDAO(session).find_one_or_none_by_id(data_id=int(user_id))
|
||||
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id))
|
||||
if not user:
|
||||
raise NoJwtException
|
||||
|
||||
@@ -58,7 +58,7 @@ async def check_refresh_token(
|
||||
async def get_current_user(
|
||||
token: str = Depends(get_access_token),
|
||||
session: AsyncSession = Depends(get_session_without_commit),
|
||||
) -> Dean:
|
||||
) -> User:
|
||||
"""Проверяем access_token и возвращаем пользователя."""
|
||||
try:
|
||||
# Декодируем токен
|
||||
@@ -80,16 +80,7 @@ async def get_current_user(
|
||||
if not user_id:
|
||||
raise NoUserIdException
|
||||
|
||||
user = await DeansDAO(session).find_one_or_none_by_id(data_id=int(user_id))
|
||||
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id))
|
||||
if not user:
|
||||
raise UserNotFoundException
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_admin_user(
|
||||
current_user: Dean = Depends(get_current_user),
|
||||
) -> Dean:
|
||||
"""Проверяем права пользователя как администратора."""
|
||||
if current_user.role.id in [3, 4]:
|
||||
return current_user
|
||||
raise ForbiddenException
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.dao.database import async_session_maker
|
||||
|
||||
from app.dao import async_session_maker
|
||||
|
||||
|
||||
async def get_session_with_commit() -> AsyncGenerator[AsyncSession, None]:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
from fastapi import FastAPI, APIRouter
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
|
||||
from app.auth.router import router as router_auth
|
||||
from app.auth import router as auth_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -53,16 +54,9 @@ def create_app() -> FastAPI:
|
||||
|
||||
def register_routers(app: FastAPI) -> None:
|
||||
"""Регистрация роутеров приложения."""
|
||||
# Корневой роутер
|
||||
root_router = APIRouter()
|
||||
|
||||
@root_router.get("/", tags=["root"])
|
||||
def home_page():
|
||||
return "Hello world"
|
||||
|
||||
# Подключение роутеров
|
||||
app.include_router(root_router, tags=["root"])
|
||||
app.include_router(router_auth, prefix="/auth", tags=["Auth"])
|
||||
app.include_router(auth_router, prefix="/auth", tags=["Auth"])
|
||||
|
||||
|
||||
# Создание экземпляра приложения
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
from alembic import context
|
||||
|
||||
from app.config import database_url
|
||||
from app.dao.database import Base
|
||||
from app.models import *
|
||||
|
||||
config = context.config
|
||||
@@ -13,7 +14,7 @@ config.set_main_option("sqlalchemy.url", database_url)
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
target_metadata = Model.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
"""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 ###
|
||||
@@ -1,5 +1,6 @@
|
||||
from .student import Student
|
||||
from .group import Group
|
||||
from .specialization import Specialization
|
||||
from .dean import Dean
|
||||
from .facult import Facult
|
||||
from .grade_book import GradeBook
|
||||
from .group import Group
|
||||
from .model import Model
|
||||
from .specialization import Specialization
|
||||
from .user import User
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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)
|
||||
8
backend/app/models/discipline.py
Normal file
8
backend/app/models/discipline.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .model import Model
|
||||
|
||||
|
||||
class Discipline(Model):
|
||||
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
|
||||
@@ -1,10 +1,10 @@
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.dao.database import Base
|
||||
from .model import Model
|
||||
|
||||
|
||||
class Facult(Base):
|
||||
class Facult(Model):
|
||||
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")
|
||||
dean_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
dean: Mapped["User"] = relationship("User", lazy="joined")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import enum
|
||||
from sqlalchemy import ForeignKey, Enum
|
||||
|
||||
from app.dao.database import Base
|
||||
from .mixins import HumanMixin
|
||||
from sqlalchemy import ForeignKey, Enum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .model import Model
|
||||
|
||||
|
||||
class StudentStatus(enum.Enum):
|
||||
STUDY = "Обучается"
|
||||
@@ -13,15 +13,17 @@ class StudentStatus(enum.Enum):
|
||||
GRADUATED = "Выпущен"
|
||||
|
||||
|
||||
class Student(HumanMixin, Base):
|
||||
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"))
|
||||
class GradeBook(Model):
|
||||
student_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
student: Mapped["User"] = relationship("User", lazy="joined")
|
||||
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"), nullable=False)
|
||||
specialization: Mapped["Specialization"] = relationship(
|
||||
"Specialization", back_populates="groups", lazy="joined"
|
||||
"Specialization", 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"
|
||||
"Group", lazy="joined"
|
||||
)
|
||||
@@ -1,12 +1,15 @@
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
from app.dao.database import Base
|
||||
from sqlalchemy import ForeignKey, Integer, CheckConstraint, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .model import Model
|
||||
|
||||
class Group(Base):
|
||||
course: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
students: Mapped[list["Student"]] = relationship(back_populates="group")
|
||||
|
||||
class Group(Model):
|
||||
course: Mapped[int] = mapped_column(Integer, CheckConstraint("course > 0"), nullable=False)
|
||||
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"))
|
||||
specialization: Mapped["Specialization"] = relationship(
|
||||
"Specialization", back_populates="groups", lazy="joined"
|
||||
"Specialization", lazy="joined"
|
||||
)
|
||||
max_students_count: Mapped[int] = mapped_column(Integer, CheckConstraint("max_students_count >= 0"), nullable=False)
|
||||
number: Mapped[int] = mapped_column(Integer, CheckConstraint("number > 0"), nullable=False)
|
||||
__table_args__ = (UniqueConstraint("course", "specialization_id", "number", name="_spec_parallel_groups_unique"),)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
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
|
||||
)
|
||||
49
backend/app/models/model.py
Normal file
49
backend/app/models/model.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Integer, inspect
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
|
||||
|
||||
|
||||
class Model(AsyncAttrs, DeclarativeBase):
|
||||
__abstract__ = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
return cls.__name__.lower() + "s"
|
||||
|
||||
def to_dict(self, exclude_none: bool = False):
|
||||
"""
|
||||
Преобразует объект модели в словарь.
|
||||
|
||||
Args:
|
||||
exclude_none (bool): Исключать ли None значения из результата
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными объекта
|
||||
"""
|
||||
result = {}
|
||||
for column in inspect(self.__class__).columns:
|
||||
value = getattr(self, column.key)
|
||||
|
||||
# Преобразование специальных типов данных
|
||||
if isinstance(value, datetime):
|
||||
value = value.isoformat()
|
||||
elif isinstance(value, Decimal):
|
||||
value = float(value)
|
||||
elif isinstance(value, uuid.UUID):
|
||||
value = str(value)
|
||||
|
||||
# Добавляем значение в результат
|
||||
if not exclude_none or value is not None:
|
||||
result[column.key] = value
|
||||
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Строковое представление объекта для удобства отладки."""
|
||||
return f"<{self.__class__.__name__}(id={self.id})>"
|
||||
@@ -1,10 +1,9 @@
|
||||
from sqlalchemy import String
|
||||
from app.dao.database import Base
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .model import Model
|
||||
|
||||
|
||||
class Specialization(Base):
|
||||
class Specialization(Model):
|
||||
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")
|
||||
|
||||
28
backend/app/models/user.py
Normal file
28
backend/app/models/user.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import String, Enum
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .model import Model
|
||||
|
||||
|
||||
class UserRole(enum.Enum):
|
||||
GUEST = "Гость"
|
||||
STUDENT = "Студент"
|
||||
TEACHER = "Преподаватель"
|
||||
DEAN = "Декан"
|
||||
RECTOR = "Ректор"
|
||||
ADMIN = "Админ"
|
||||
|
||||
|
||||
class User(Model):
|
||||
email: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
password: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
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
|
||||
)
|
||||
role: Mapped[UserRole] = mapped_column(
|
||||
Enum(UserRole), nullable=False, default=UserRole.GUEST
|
||||
)
|
||||
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
32
backend/app/routers/user.py
Normal file
32
backend/app/routers/user.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas import SUserInfo
|
||||
from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit
|
||||
|
||||
router = APIRouter(prefix="/user")
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
async def get_user_by_id(user_id: int, session: AsyncSession = Depends(get_session_without_commit)) -> SUserInfo:
|
||||
pass
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_users(session: AsyncSession = Depends(get_session_without_commit)) -> list[SUserInfo]:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_user(user_data, session: AsyncSession = Depends(get_session_with_commit)) -> dict:
|
||||
pass
|
||||
|
||||
|
||||
@router.put("/{user_id}")
|
||||
async def update_user(user_id: int, user_data, session: AsyncSession = Depends(get_session_with_commit)):
|
||||
pass
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(user_id: int, session: AsyncSession = Depends(get_session_with_commit)):
|
||||
pass
|
||||
2
backend/app/schemas/__init__.py
Normal file
2
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .general import EmailModel
|
||||
from .user import UserBase, SUserInfo, SUserAddDB, SUserRegister, SUserAuth
|
||||
11
backend/app/schemas/general.py
Normal file
11
backend/app/schemas/general.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
EmailStr,
|
||||
Field,
|
||||
)
|
||||
|
||||
|
||||
class EmailModel(BaseModel):
|
||||
email: EmailStr = Field(description="Электронная почта")
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,17 +1,13 @@
|
||||
from typing import Self
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
EmailStr,
|
||||
Field,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from app.auth.utils import get_password_hash
|
||||
|
||||
|
||||
class EmailModel(BaseModel):
|
||||
email: EmailStr = Field(description="Электронная почта")
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
from app.models.user import UserRole
|
||||
from .general import EmailModel
|
||||
|
||||
|
||||
class UserBase(EmailModel):
|
||||
@@ -27,9 +23,10 @@ class UserBase(EmailModel):
|
||||
description="Отчество, от 3 до 50 символов",
|
||||
default=None,
|
||||
)
|
||||
role: UserRole = Field(description="Роль пользователя в системе", default=UserRole.GUEST)
|
||||
|
||||
|
||||
class SDeanRegister(UserBase):
|
||||
class SUserRegister(UserBase):
|
||||
password: str = Field(
|
||||
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
|
||||
)
|
||||
@@ -47,15 +44,15 @@ class SDeanRegister(UserBase):
|
||||
return self
|
||||
|
||||
|
||||
class SDeanAddDB(UserBase):
|
||||
class SUserAddDB(UserBase):
|
||||
password: str = Field(min_length=5, description="Пароль в формате HASH-строки")
|
||||
|
||||
|
||||
class SDeanAuth(EmailModel):
|
||||
class SUserAuth(EmailModel):
|
||||
password: str = Field(
|
||||
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
|
||||
)
|
||||
|
||||
|
||||
class SDeanInfo(UserBase):
|
||||
class SUserInfo(UserBase):
|
||||
id: int = Field(description="Идентификатор пользователя")
|
||||
@@ -53,6 +53,6 @@ typer==0.16.0
|
||||
typing_extensions==4.14.0
|
||||
ujson==5.10.0
|
||||
uvicorn==0.31.0
|
||||
uvloop==0.21.0
|
||||
# uvloop==0.21.0
|
||||
watchfiles==1.0.5
|
||||
websockets==15.0.1
|
||||
|
||||
Reference in New Issue
Block a user