diff --git a/Makefile b/Makefile index d10e3fe..77fcdfa 100644 --- a/Makefile +++ b/Makefile @@ -21,4 +21,4 @@ redis: sleep 3 migrate: - alembic upgrade head + source ./venv/bin/activate && alembic upgrade head diff --git a/migrations/versions/41e3e1eb6c9d_remove_union_member.py b/migrations/versions/41e3e1eb6c9d_remove_union_member.py new file mode 100644 index 0000000..dbc466e --- /dev/null +++ b/migrations/versions/41e3e1eb6c9d_remove_union_member.py @@ -0,0 +1,37 @@ +"""remove union_member + +Revision ID: 41e3e1eb6c9d +Revises: a68c6bb2972c +Create Date: 2025-10-07 19:40:11.770337 + +""" + +import sqlalchemy as sa +from alembic import op + + +revision = '41e3e1eb6c9d' +down_revision = 'a68c6bb2972c' +branch_labels = None +depends_on = None + + +def upgrade(): + op.drop_constraint('file_owner_id_fkey', 'file', type_='foreignkey') + op.drop_constraint('print_fact_owner_id_fkey', 'print_fact', type_='foreignkey') + op.drop_table('union_member') + op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=False) + + +def downgrade(): + op.alter_column('file', 'source', existing_type=sa.VARCHAR(), nullable=True) + op.create_table( + 'union_member', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('surname', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('union_number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('student_number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='union_member_pkey'), + ) + op.create_foreign_key('file_owner_id_fkey', 'file', 'union_member', ['owner_id'], ['id']) + op.create_foreign_key('print_fact_owner_id_fkey', 'print_fact', 'union_member', ['owner_id'], ['id']) diff --git a/print_service/models/__init__.py b/print_service/models/__init__.py index d1ffcfe..5b32c02 100644 --- a/print_service/models/__init__.py +++ b/print_service/models/__init__.py @@ -5,7 +5,7 @@ from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.ext.declarative import as_declarative -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, relationship from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql.sqltypes import Boolean @@ -15,25 +15,13 @@ class Model: pass -class UnionMember(Model): - __tablename__ = 'union_member' - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - surname: Mapped[str] = mapped_column(String, nullable=False) - union_number: Mapped[str] = mapped_column(String, nullable=True) - student_number: Mapped[str] = mapped_column(String, nullable=True) - - files: Mapped[list[File]] = relationship('File', back_populates='owner') - print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='owner') - - class File(Model): __tablename__ = 'file' id: Mapped[int] = Column(Integer, primary_key=True) pin: Mapped[str] = Column(String, nullable=False) file: Mapped[str] = Column(String, nullable=False) - owner_id: Mapped[int] = Column(Integer, ForeignKey('union_member.id'), nullable=False) + owner_id: Mapped[int] = Column(Integer, nullable=False) option_pages: Mapped[str] = Column(String) option_copies: Mapped[int] = Column(Integer) option_two_sided: Mapped[bool] = Column(Boolean) @@ -44,7 +32,6 @@ class File(Model): number_of_pages: Mapped[int] = Column(Integer) source: Mapped[str] = Column(String, default='unknown', nullable=False) - owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='files') print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='file') @property @@ -84,9 +71,8 @@ class PrintFact(Model): id: Mapped[int] = Column(Integer, primary_key=True) file_id: Mapped[int] = Column(Integer, ForeignKey('file.id'), nullable=False) - owner_id: Mapped[int] = Column(Integer, ForeignKey('union_member.id'), nullable=False) + owner_id: Mapped[int] = Column(Integer, nullable=False) created_at: Mapped[datetime] = Column(DateTime, nullable=False, default=datetime.utcnow) - owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='print_facts') file: Mapped[File] = relationship('File', back_populates='print_facts') sheets_used: Mapped[int] = Column(Integer) diff --git a/print_service/routes/admin.py b/print_service/routes/admin.py index da2b7b1..25cef4e 100644 --- a/print_service/routes/admin.py +++ b/print_service/routes/admin.py @@ -2,7 +2,7 @@ import logging from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from redis import Redis from print_service.exceptions import TerminalTokenNotFound diff --git a/print_service/routes/base.py b/print_service/routes/base.py index 37def7b..17574ee 100644 --- a/print_service/routes/base.py +++ b/print_service/routes/base.py @@ -9,7 +9,6 @@ from print_service.routes.admin import router as admin_router from print_service.routes.file import router as file_router from print_service.routes.qrprint import router as qrprint_router -from print_service.routes.user import router as user_router from print_service.settings import Settings, get_settings @@ -40,7 +39,6 @@ ) -app.include_router(user_router, prefix='', tags=['User']) app.include_router(file_router, prefix='/file', tags=['File']) app.include_router(qrprint_router, prefix='/qr', tags=['File']) app.include_router(admin_router, prefix='/admin', tags=['Admin']) diff --git a/print_service/routes/exc_handlers.py b/print_service/routes/exc_handlers.py index ecb8aeb..0a0ab79 100644 --- a/print_service/routes/exc_handlers.py +++ b/print_service/routes/exc_handlers.py @@ -1,4 +1,3 @@ -import requests.models import starlette.requests from starlette.responses import JSONResponse diff --git a/print_service/routes/file.py b/print_service/routes/file.py index 21e3570..af3a405 100644 --- a/print_service/routes/file.py +++ b/print_service/routes/file.py @@ -6,11 +6,10 @@ import aiofiles.os from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, File, UploadFile -from fastapi.exceptions import HTTPException from fastapi.params import Depends from fastapi_sqlalchemy import db from pydantic import Field, field_validator -from sqlalchemy import func, or_ +from sqlalchemy import func from print_service.base import StatusResponseModel from print_service.exceptions import ( @@ -19,16 +18,12 @@ InvalidPageRequest, InvalidType, IsCorrupted, - NotInUnion, PINGenerateError, PINNotFound, TooLargeSize, TooManyPages, - UnprocessableFileInstance, - UserNotFound, ) from print_service.models import File as FileModel -from print_service.models import UnionMember from print_service.schema import BaseModel from print_service.settings import Settings, get_settings from print_service.utils import checking_for_pdf, generate_filename, generate_pin, get_file @@ -60,16 +55,6 @@ def validate_pages(cls, value: str): class SendInput(BaseModel): - surname: str | None = Field( - default=None, - description='Фамилия', - example='Иванов', - ) - number: str | None = Field( - default=None, - description='Номер профсоюзного или студенческого билетов', - example='1015000', - ) filename: str = Field( description='Название файла', example='filename.pdf', @@ -112,41 +97,21 @@ class ReceiveOutput(BaseModel): ) async def send( inp: SendInput, - user_auth=Depends(UnionAuth(allow_none=True)), - settings: Settings = Depends(get_settings), + user_auth=Depends(UnionAuth(scopes=["print.service.use"])), ): """Получить пин код для загрузки и скачивания файла. + Scopes: `["print.service.use"]` + Полученный пин-код можно использовать в методах POST и GET `/file/{pin}`. """ - user = db.session.query(UnionMember) - if not settings.ALLOW_STUDENT_NUMBER: - user = user.filter(UnionMember.union_number != None) - - if (inp.number is not None) and (inp.surname is not None): - user = user.filter( - or_( - func.upper(UnionMember.student_number) == inp.number.upper(), - func.upper(UnionMember.union_number) == inp.number.upper(), - ), - func.upper(UnionMember.surname) == inp.surname.upper(), - ) - - else: - if not "print.file.send" in [scope["name"] for scope in user_auth.get('session_scopes')]: - raise NotInUnion() - - user = user.one_or_none() - - if user is None: - raise NotInUnion() try: pin = generate_pin(db.session) except RuntimeError: raise PINGenerateError() filename = generate_filename(inp.filename) file_model = FileModel(pin=pin, file=filename, source=inp.source) - file_model.owner = user + file_model.owner_id = user_auth['id'] file_model.option_copies = inp.options.copies file_model.option_pages = inp.options.pages file_model.option_two_sided = inp.options.two_sided diff --git a/print_service/routes/user.py b/print_service/routes/user.py deleted file mode 100644 index 34a3a3c..0000000 --- a/print_service/routes/user.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -from typing import List, Optional - -from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends -from fastapi.exceptions import HTTPException -from fastapi_sqlalchemy import db -from pydantic import constr, validate_call -from sqlalchemy import and_, func, or_ - -from print_service import __version__ -from print_service.exceptions import UnionStudentDuplicate, UserNotFound -from print_service.models import UnionMember -from print_service.schema import BaseModel -from print_service.settings import get_settings - - -logger = logging.getLogger(__name__) -router = APIRouter() -settings = get_settings() - - -# region schemas -class UserCreate(BaseModel): - username: constr(strip_whitespace=True, to_upper=True, min_length=1) - union_number: Optional[constr(strip_whitespace=True, to_upper=True, min_length=1)] - student_number: Optional[constr(strip_whitespace=True, to_upper=True, min_length=1)] - - -class UpdateUserList(BaseModel): - users: List[UserCreate] - - -# endregion - - -# region handlers - - -@router.get( - '/is_union_member', - status_code=202, - responses={ - 404: {'detail': 'User not found'}, - }, -) -async def check_union_member( - surname: constr(strip_whitespace=True, to_upper=True, min_length=1), - number: constr(strip_whitespace=True, to_upper=True, min_length=1), - v: Optional[str] = __version__, -): - """Проверяет наличие пользователя в списке.""" - - surname = surname.upper() - user = db.session.query(UnionMember) - if not settings.ALLOW_STUDENT_NUMBER: - user = user.filter(UnionMember.union_number != None) - user: UnionMember = user.filter( - or_( - func.upper(UnionMember.student_number) == number, - func.upper(UnionMember.union_number) == number, - ), - func.upper(UnionMember.surname) == surname, - ).one_or_none() - - if v == '1': - return bool(user) - - if not user: - raise UserNotFound() - else: - return { - 'surname': user.surname, - 'number': number, - 'student_number': user.student_number, - 'union_number': user.union_number, - } - - -@router.post('/is_union_member') -def update_list( - input: UpdateUserList, - user=Depends(UnionAuth(scopes=["print.user.create", "print.user.update", "print.user.delete"])), -): - """Обновляет данные существующего пользователя или добавляет нового, если его нет.""" - logger.info(f"User {user} updated list") - - union_numbers = [user.union_number for user in input.users if user.union_number is not None] - student_numbers = [user.student_number for user in input.users if user.student_number is not None] - - if len(union_numbers) != len(set(union_numbers)) or len(student_numbers) != len( - set(student_numbers) - ): - raise UnionStudentDuplicate() - - for user in input.users: - db_user: UnionMember = ( - db.session.query(UnionMember) - .filter( - or_( - and_( - UnionMember.union_number == user.union_number, - UnionMember.union_number != None, - ), - and_( - UnionMember.student_number == user.student_number, - UnionMember.student_number != None, - ), - ) - ) - .one_or_none() - ) - - if db_user: - db_user.surname = user.username - db_user.union_number = user.union_number - db_user.student_number = user.student_number - else: - db.session.add( - UnionMember( - surname=user.username, - union_number=user.union_number, - student_number=user.student_number, - ) - ) - db.session.flush() - - db.session.commit() - return {"status": "ok", "count": len(input.users)} - - -# endregion diff --git a/print_service/settings.py b/print_service/settings.py index c7de741..1755fac 100644 --- a/print_service/settings.py +++ b/print_service/settings.py @@ -4,7 +4,7 @@ from typing import List from auth_lib.fastapi import UnionAuthSettings -from pydantic import AnyUrl, ConfigDict, DirectoryPath, PostgresDsn, RedisDsn +from pydantic import ConfigDict, DirectoryPath, PostgresDsn, RedisDsn from pydantic_settings import BaseSettings diff --git a/print_service/utils/__init__.py b/print_service/utils/__init__.py index 1c44b33..70a9bb7 100644 --- a/print_service/utils/__init__.py +++ b/print_service/utils/__init__.py @@ -1,12 +1,10 @@ import io -import math import random import re from datetime import date, datetime, timedelta from os.path import abspath, exists from fastapi import File -from fastapi.exceptions import HTTPException from PyPDF4 import PdfFileReader from sqlalchemy import func from sqlalchemy.orm.session import Session @@ -20,7 +18,6 @@ from print_service.models import File from print_service.models import File as FileModel from print_service.models import PrintFact -from print_service.routes import exc_handlers from print_service.settings import Settings, get_settings