сделал репозиторий для проектов, сделал полностью управление фотографиями профиля

This commit is contained in:
Андрей Дувакин 2025-05-08 10:11:12 +05:00
parent 2903330a2f
commit 0aee1e625b
16 changed files with 348 additions and 20 deletions

View File

@ -0,0 +1,32 @@
from typing import Optional, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import ProfilePhoto
class ProfilePhotosRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, photo_id: int) -> Optional[ProfilePhoto]:
stmt = select(ProfilePhoto).filter_by(id=photo_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def get_by_profile_id(self, profile_id: int) -> Sequence[ProfilePhoto]:
stmt = select(ProfilePhoto).filter_by(profile_id=profile_id)
result = await self.db.execute(stmt)
return result.scalars().all()
async def create(self, photo: ProfilePhoto) -> ProfilePhoto:
self.db.add(photo)
await self.db.commit()
await self.db.refresh(photo)
return photo
async def delete(self, photo: ProfilePhoto) -> ProfilePhoto:
await self.db.delete(photo)
await self.db.commit()
return photo

View File

@ -0,0 +1,37 @@
from typing import Optional, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Project
class ProjectsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[Project]:
stmt = select(Project)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_id(self, project_id: int) -> Optional[Project]:
stmt = select(Project).filter_by(id=project_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def create(self, project: Project) -> Project:
self.db.add(project)
await self.db.commit()
await self.db.refresh(project)
return project
async def update(self, project: Project) -> Project:
await self.db.merge(project)
await self.db.commit()
return project
async def delete(self, project: Project) -> Project:
await self.db.delete(project)
await self.db.commit()
return project

View File

@ -0,0 +1,69 @@
from fastapi import Depends, File, UploadFile, APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.database.session import get_db
from app.domain.entities.profile_photo import ProfilePhotoEntity
from app.infrastructure.dependencies import get_current_user, require_admin
from app.infrastructure.profile_photos_service import ProfilePhotosService
router = APIRouter()
@router.get(
"/profiles/{profile_id}/",
response_model=list[ProfilePhotoEntity],
summary="Get all photos metadata for a profile",
description="Returns metadata of all profile photos for the given profile_id",
)
async def get_photos_by_profile_id(
profile_id: int,
db: AsyncSession = Depends(get_db),
):
service = ProfilePhotosService(db)
return await service.get_photo_file_by_profile_id(profile_id)
@router.get(
"/{photo_id}/file",
response_class=FileResponse,
summary="Download photo file by photo ID",
description="Returns the image file for the given photo ID",
)
async def download_photo_file(
photo_id: int,
db: AsyncSession = Depends(get_db),
):
service = ProfilePhotosService(db)
return await service.get_photo_file_by_id(photo_id)
@router.post(
"/profiles/{profile_id}/upload",
response_model=ProfilePhotoEntity,
summary="Upload a new photo for a profile",
description="Uploads a new photo file and associates it with the given profile ID",
)
async def upload_photo(
profile_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProfilePhotosService(db)
return await service.upload_photo(profile_id, file, user)
@router.delete(
"/{photo_id}/",
response_model=ProfilePhotoEntity,
summary="Delete a photo by ID",
description="Deletes a photo and its file from storage",
)
async def delete_photo(
photo_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProfilePhotosService(db)
return await service.delete_photo(photo_id, user)

View File

@ -17,7 +17,7 @@ router = APIRouter()
summary='Create a new profile', summary='Create a new profile',
description='Creates a new profile', description='Creates a new profile',
) )
async def create_team( async def create_profile(
profile: ProfileEntity, profile: ProfileEntity,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(require_admin), user=Depends(require_admin),
@ -32,11 +32,11 @@ async def create_team(
summary='Update a profile', summary='Update a profile',
description='Updates a profile', description='Updates a profile',
) )
async def create_team( async def update_profile(
profile_id: int, profile_id: int,
profile: ProfileEntity, profile: ProfileEntity,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(require_admin), user=Depends(get_current_user),
): ):
profiles_service = ProfilesService(db) profiles_service = ProfilesService(db)
return await profiles_service.update_profile(profile_id, profile, user) return await profiles_service.update_profile(profile_id, profile, user)
@ -48,7 +48,7 @@ async def create_team(
summary='Delete a profile', summary='Delete a profile',
description='Delete a profile', description='Delete a profile',
) )
async def create_team( async def delete_profile(
profile_id: int, profile_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(require_admin), user=Depends(require_admin),

View File

@ -46,11 +46,11 @@ async def create_team(
summary='Update a team', summary='Update a team',
description='Updates a team', description='Updates a team',
) )
async def create_team( async def update_team(
team_id: int, team_id: int,
team: TeamEntity, team: TeamEntity,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), user=Depends(require_admin),
): ):
teams_service = TeamsService(db) teams_service = TeamsService(db)
return await teams_service.update_team(team_id, team) return await teams_service.update_team(team_id, team)
@ -62,7 +62,7 @@ async def create_team(
summary='Delete a team', summary='Delete a team',
description='Delete a team', description='Delete a team',
) )
async def create_team( async def delete_team(
team_id: int, team_id: int,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(require_admin), user=Depends(require_admin),

View File

@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db from app.database.session import get_db
from app.domain.entities.user import UserEntity from app.domain.entities.user import UserEntity
from app.infrastructure.dependencies import require_admin from app.infrastructure.dependencies import get_current_user
from app.infrastructure.users_service import UsersService from app.infrastructure.users_service import UsersService
router = APIRouter() router = APIRouter()
@ -17,11 +17,11 @@ router = APIRouter()
summary='Change user password', summary='Change user password',
description='Change user password', description='Change user password',
) )
async def create_team( async def create_user(
user_id: int, user_id: int,
new_password: str, new_password: str,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(require_admin), user=Depends(get_current_user),
): ):
users_service = UsersService(db) users_service = UsersService(db)
return await users_service.change_user_password(user_id, new_password) return await users_service.change_user_password(user_id, new_password, user)

View File

@ -0,0 +1,32 @@
"""0002_добавил_поле_filename_уотографий_профиля
Revision ID: b4056dda0936
Revises: d53be3a35511
Create Date: 2025-05-05 21:06:32.080386
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b4056dda0936'
down_revision: Union[str, None] = 'd53be3a35511'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('profile_photos', sa.Column('filename', sa.String(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('profile_photos', 'filename')
# ### end Alembic commands ###

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel
class ProfilePhotoEntity(BaseModel):
id: int
filename: str
file_path: str
profile_id: int

View File

@ -0,0 +1,9 @@
from typing import Optional
from pydantic import BaseModel
class ProjectEntity(BaseModel):
id: Optional[int] = None
description: str
repository_url: Optional[str] = None

View File

@ -7,6 +7,7 @@ from app.domain.models.base import AdvancedBaseModel
class ProfilePhoto(AdvancedBaseModel): class ProfilePhoto(AdvancedBaseModel):
__tablename__ = 'profile_photos' __tablename__ = 'profile_photos'
filename = Column(String, nullable=False)
file_path = Column(String, nullable=False) file_path = Column(String, nullable=False)
profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False) profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False)

View File

@ -0,0 +1,126 @@
import os
import uuid
import aiofiles
import magic
from fastapi import HTTPException, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from werkzeug.utils import secure_filename
from app.application.profile_photos_repository import ProfilePhotosRepository
from app.application.users_repository import UsersRepository
from app.domain.entities.profile_photo import ProfilePhotoEntity
from app.domain.models import ProfilePhoto, User
class ProfilePhotosService:
def __init__(self, db: AsyncSession):
self.profile_photos_repository = ProfilePhotosRepository(db)
self.users_repository = UsersRepository(db)
async def get_photo_file_by_id(self, photo_id: int) -> FileResponse:
photo = await self.profile_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(404, "Photo not found")
if not os.path.exists(photo.file_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
photo.file_path,
media_type=self.get_media_type(photo.filename),
filename=photo.filename,
)
async def get_photo_file_by_profile_id(self, profile_id: int) -> list[ProfilePhotoEntity]:
photos = await self.profile_photos_repository.get_by_profile_id(profile_id)
return [
self.model_to_entity(photo)
for photo in photos
]
async def upload_photo(self, profile_id: int, file: UploadFile, user: User):
user = await self.users_repository.get_by_id(user.id)
if profile_id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
self.validate_file_type(file)
photo = ProfilePhoto(
filename=self.generate_filename(file),
file_path=await self.save_file(file),
profile_id=profile_id
)
return self.model_to_entity(
await self.profile_photos_repository.create(photo)
)
async def delete_photo(self, photo_id: int, user: User) -> ProfilePhotoEntity:
user = await self.users_repository.get_by_id(user.id)
if user is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this ID was not found",
)
photo = await self.profile_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(404, "Photo not found")
if photo.profile_id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
if os.path.exists(photo.file_path):
os.remove(photo.file_path)
return self.model_to_entity(
await self.profile_photos_repository.delete(photo)
)
async def save_file(self, file: UploadFile, upload_dir: str = "uploads/profile_photos") -> str:
os.makedirs(upload_dir, exist_ok=True)
filename = self.generate_filename(file)
file_path = os.path.join(upload_dir, filename)
async with aiofiles.open(file_path, 'wb') as out_file:
content = await file.read()
await out_file.write(content)
return file_path
@staticmethod
def validate_file_type(file: UploadFile):
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(file.file.read(1024))
file.file.seek(0)
if file_type not in ["image/jpeg", "image/png"]:
raise HTTPException(400, "Invalid file type")
@staticmethod
def generate_filename(file: UploadFile):
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
@staticmethod
def model_to_entity(profile_photo_model: ProfilePhoto) -> ProfilePhotoEntity:
return ProfilePhotoEntity(
id=profile_photo_model.id,
filename=profile_photo_model.filename,
file_path=profile_photo_model.file_path,
profile_id=profile_photo_model.profile_id,
)
@staticmethod
def get_media_type(filename: str) -> str:
extension = filename.split('.')[-1].lower()
return f"image/{extension}" if extension in ['jpeg', 'jpg', 'png'] else "application/octet-stream"

View File

@ -23,14 +23,14 @@ class ProfilesService:
if team is None: if team is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The team with this ID was not found", detail='The team with this ID was not found',
) )
role = await self.roles_repository.get_by_id(profile.role_id) role = await self.roles_repository.get_by_id(profile.role_id)
if role is None: if role is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The role with this ID was not found", detail='The role with this ID was not found',
) )
profile_model = self.entity_to_model(profile) profile_model = self.entity_to_model(profile)
@ -46,28 +46,28 @@ class ProfilesService:
if user is None: if user is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this ID was not found", detail='The user with this ID was not found',
) )
profile_model = await self.profiles_repository.get_by_id(profile_id) profile_model = await self.profiles_repository.get_by_id(profile_id)
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор': if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied", detail='Permission denied',
) )
team = await self.teams_repository.get_by_id(profile.team_id) team = await self.teams_repository.get_by_id(profile.team_id)
if team is None: if team is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The team with this ID was not found", detail='The team with this ID was not found',
) )
role = await self.roles_repository.get_by_id(profile.role_id) role = await self.roles_repository.get_by_id(profile.role_id)
if role is None: if role is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The role with this ID was not found", detail='The role with this ID was not found',
) )
profile_model.first_name = profile.first_name profile_model.first_name = profile.first_name
@ -88,14 +88,14 @@ class ProfilesService:
if user is None: if user is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this ID was not found", detail='The user with this ID was not found',
) )
profile_model = await self.profiles_repository.get_by_id(profile_id) profile_model = await self.profiles_repository.get_by_id(profile_id)
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор': if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied", detail='Permission denied',
) )
result = await self.profiles_repository.delete(profile_model) result = await self.profiles_repository.delete(profile_model)

View File

@ -0,0 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
class ProjectsRepository:
def __init__(self, db: AsyncSession):
self.projects_repository = ProjectsRepository(db)

View File

@ -52,7 +52,7 @@ class UsersService:
return user_entity return user_entity
async def change_user_password(self, user_id: int, new_password: str) -> Optional[UserEntity]: async def change_user_password(self, user_id: int, new_password: str, user: User) -> Optional[UserEntity]:
user_model = await self.users_repository.get_by_id(user_id) user_model = await self.users_repository.get_by_id(user_id)
if user_model is None: if user_model is None:
@ -61,6 +61,12 @@ class UsersService:
detail="The user with this ID was not found", detail="The user with this ID was not found",
) )
if user_model.id != user_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
if not self.is_strong_password(new_password): if not self.is_strong_password(new_password):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.contollers.auth_router import router as auth_router from app.contollers.auth_router import router as auth_router
from app.contollers.profile_photos_router import router as profile_photos_router
from app.contollers.profiles_router import router as profiles_router from app.contollers.profiles_router import router as profiles_router
from app.contollers.register_router import router as register_router from app.contollers.register_router import router as register_router
from app.contollers.teams_router import router as team_router from app.contollers.teams_router import router as team_router
@ -21,6 +22,7 @@ def start_app():
) )
api_app.include_router(auth_router, prefix=f'{settings.PREFIX}/auth', tags=['auth']) api_app.include_router(auth_router, prefix=f'{settings.PREFIX}/auth', tags=['auth'])
api_app.include_router(profile_photos_router, prefix=f'{settings.PREFIX}/profile_photos', tags=['profile_photos_router'])
api_app.include_router(profiles_router, prefix=f'{settings.PREFIX}/profiles', tags=['profiles']) api_app.include_router(profiles_router, prefix=f'{settings.PREFIX}/profiles', tags=['profiles'])
api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register']) api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams']) api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])

Binary file not shown.