сделал репозиторий для проектов, сделал полностью управление фотографиями профиля
This commit is contained in:
parent
2903330a2f
commit
0aee1e625b
32
API/app/application/profile_photos_repository.py
Normal file
32
API/app/application/profile_photos_repository.py
Normal 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
|
||||
37
API/app/application/projects_repository.py
Normal file
37
API/app/application/projects_repository.py
Normal 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
|
||||
69
API/app/contollers/profile_photos_router.py
Normal file
69
API/app/contollers/profile_photos_router.py
Normal 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)
|
||||
@ -17,7 +17,7 @@ router = APIRouter()
|
||||
summary='Create a new profile',
|
||||
description='Creates a new profile',
|
||||
)
|
||||
async def create_team(
|
||||
async def create_profile(
|
||||
profile: ProfileEntity,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
@ -32,11 +32,11 @@ async def create_team(
|
||||
summary='Update a profile',
|
||||
description='Updates a profile',
|
||||
)
|
||||
async def create_team(
|
||||
async def update_profile(
|
||||
profile_id: int,
|
||||
profile: ProfileEntity,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
profiles_service = ProfilesService(db)
|
||||
return await profiles_service.update_profile(profile_id, profile, user)
|
||||
@ -48,7 +48,7 @@ async def create_team(
|
||||
summary='Delete a profile',
|
||||
description='Delete a profile',
|
||||
)
|
||||
async def create_team(
|
||||
async def delete_profile(
|
||||
profile_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
|
||||
@ -46,11 +46,11 @@ async def create_team(
|
||||
summary='Update a team',
|
||||
description='Updates a team',
|
||||
)
|
||||
async def create_team(
|
||||
async def update_team(
|
||||
team_id: int,
|
||||
team: TeamEntity,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
teams_service = TeamsService(db)
|
||||
return await teams_service.update_team(team_id, team)
|
||||
@ -62,7 +62,7 @@ async def create_team(
|
||||
summary='Delete a team',
|
||||
description='Delete a team',
|
||||
)
|
||||
async def create_team(
|
||||
async def delete_team(
|
||||
team_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
|
||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.session import get_db
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
@ -17,11 +17,11 @@ router = APIRouter()
|
||||
summary='Change user password',
|
||||
description='Change user password',
|
||||
)
|
||||
async def create_team(
|
||||
async def create_user(
|
||||
user_id: int,
|
||||
new_password: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
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)
|
||||
|
||||
@ -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 ###
|
||||
8
API/app/domain/entities/profile_photo.py
Normal file
8
API/app/domain/entities/profile_photo.py
Normal file
@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ProfilePhotoEntity(BaseModel):
|
||||
id: int
|
||||
filename: str
|
||||
file_path: str
|
||||
profile_id: int
|
||||
9
API/app/domain/entities/project.py
Normal file
9
API/app/domain/entities/project.py
Normal 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
|
||||
@ -7,6 +7,7 @@ from app.domain.models.base import AdvancedBaseModel
|
||||
class ProfilePhoto(AdvancedBaseModel):
|
||||
__tablename__ = 'profile_photos'
|
||||
|
||||
filename = Column(String, nullable=False)
|
||||
file_path = Column(String, nullable=False)
|
||||
|
||||
profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False)
|
||||
|
||||
126
API/app/infrastructure/profile_photos_service.py
Normal file
126
API/app/infrastructure/profile_photos_service.py
Normal 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"
|
||||
@ -23,14 +23,14 @@ class ProfilesService:
|
||||
if team is None:
|
||||
raise HTTPException(
|
||||
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)
|
||||
if role is None:
|
||||
raise HTTPException(
|
||||
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)
|
||||
@ -46,28 +46,28 @@ class ProfilesService:
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
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)
|
||||
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Permission denied",
|
||||
detail='Permission denied',
|
||||
)
|
||||
|
||||
team = await self.teams_repository.get_by_id(profile.team_id)
|
||||
if team is None:
|
||||
raise HTTPException(
|
||||
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)
|
||||
if role is None:
|
||||
raise HTTPException(
|
||||
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
|
||||
@ -88,14 +88,14 @@ class ProfilesService:
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
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)
|
||||
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Permission denied",
|
||||
detail='Permission denied',
|
||||
)
|
||||
|
||||
result = await self.profiles_repository.delete(profile_model)
|
||||
|
||||
6
API/app/infrastructure/projects_service.py
Normal file
6
API/app/infrastructure/projects_service.py
Normal file
@ -0,0 +1,6 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class ProjectsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.projects_repository = ProjectsRepository(db)
|
||||
@ -52,7 +52,7 @@ class UsersService:
|
||||
|
||||
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)
|
||||
|
||||
if user_model is None:
|
||||
@ -61,6 +61,12 @@ class UsersService:
|
||||
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):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
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.register_router import router as register_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(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(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
|
||||
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
|
||||
|
||||
BIN
API/req.txt
BIN
API/req.txt
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user