сделал редактирование файлов у конкурсов и проектов
This commit is contained in:
parent
03338413c1
commit
56725b681b
32
API/app/application/contest_files_repository.py
Normal file
32
API/app/application/contest_files_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 ContestFile
|
||||||
|
|
||||||
|
|
||||||
|
class ContestFilesRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_by_id(self, file_id: int) -> Optional[ContestFile]:
|
||||||
|
stmt = select(ContestFile).filter_by(id=file_id)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_by_contest_id(self, contest_id: int) -> Sequence[ContestFile]:
|
||||||
|
stmt = select(ContestFile).filter_by(contest_id=contest_id)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def create(self, contest_file: ContestFile) -> ContestFile:
|
||||||
|
self.db.add(contest_file)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(contest_file)
|
||||||
|
return contest_file
|
||||||
|
|
||||||
|
async def delete(self, contest_file: ContestFile) -> ContestFile:
|
||||||
|
await self.db.delete(contest_file)
|
||||||
|
await self.db.commit()
|
||||||
|
return contest_file
|
||||||
69
API/app/contollers/contest_files_router.py
Normal file
69
API/app/contollers/contest_files_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.contest_file import ContestFileEntity
|
||||||
|
from app.infrastructure.dependencies import require_admin
|
||||||
|
from app.infrastructure.contest_files_service import ContestFilesService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/contests/{contest_id}/",
|
||||||
|
response_model=list[ContestFileEntity],
|
||||||
|
summary="Get all contest files",
|
||||||
|
description="Returns metadata of all files uploaded for the specified contest."
|
||||||
|
)
|
||||||
|
async def get_files_by_contest_id(
|
||||||
|
contest_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
service = ContestFilesService(db)
|
||||||
|
return await service.get_files_by_contest_id(contest_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{file_id}/file",
|
||||||
|
response_class=FileResponse,
|
||||||
|
summary="Download contest file by ID",
|
||||||
|
description="Returns the file for the specified file ID."
|
||||||
|
)
|
||||||
|
async def download_contest_file(
|
||||||
|
file_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
service = ContestFilesService(db)
|
||||||
|
return await service.get_file_by_id(file_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/contests/{contest_id}/upload",
|
||||||
|
response_model=ContestFileEntity,
|
||||||
|
summary="Upload a new file for the contest",
|
||||||
|
description="Uploads a new file and associates it with the specified contest."
|
||||||
|
)
|
||||||
|
async def upload_contest_file(
|
||||||
|
contest_id: int,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
service = ContestFilesService(db)
|
||||||
|
return await service.upload_file(contest_id, file, user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{file_id}/",
|
||||||
|
response_model=ContestFileEntity,
|
||||||
|
summary="Delete a contest file by ID",
|
||||||
|
description="Deletes the file and its database entry."
|
||||||
|
)
|
||||||
|
async def delete_contest_file(
|
||||||
|
file_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
service = ContestFilesService(db)
|
||||||
|
return await service.delete_file(file_id, user)
|
||||||
@ -4,7 +4,7 @@ from starlette.responses import FileResponse
|
|||||||
|
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
from app.domain.entities.project_file import ProjectFileEntity
|
from app.domain.entities.project_file import ProjectFileEntity
|
||||||
from app.infrastructure.dependencies import get_current_user, require_admin
|
from app.infrastructure.dependencies import require_admin
|
||||||
from app.infrastructure.project_files_service import ProjectFilesService
|
from app.infrastructure.project_files_service import ProjectFilesService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
"""0005_добавил_поле_filename_у_файлов_конкурсов
|
||||||
|
|
||||||
|
Revision ID: de2777da99c9
|
||||||
|
Revises: e53896c51cf8
|
||||||
|
Create Date: 2025-06-03 10:23:08.013605
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'de2777da99c9'
|
||||||
|
down_revision: Union[str, None] = 'e53896c51cf8'
|
||||||
|
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('contest_files', 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('contest_files', 'filename')
|
||||||
|
# ### end Alembic commands ###
|
||||||
10
API/app/domain/entities/contest_file.py
Normal file
10
API/app/domain/entities/contest_file.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ContestFileEntity(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
filename: str
|
||||||
|
file_path: str
|
||||||
|
contest_id: int
|
||||||
@ -1,8 +1,10 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class ProfilePhotoEntity(BaseModel):
|
class ProfilePhotoEntity(BaseModel):
|
||||||
id: int
|
id: Optional[int] = None
|
||||||
filename: str
|
filename: str
|
||||||
file_path: str
|
file_path: str
|
||||||
profile_id: int
|
profile_id: int
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class ProjectFileEntity(BaseModel):
|
class ProjectFileEntity(BaseModel):
|
||||||
id: int
|
id: Optional[int] = None
|
||||||
filename: str
|
filename: str
|
||||||
file_path: str
|
file_path: str
|
||||||
project_id: int
|
project_id: int
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from app.domain.models.base import AdvancedBaseModel
|
|||||||
class ContestFile(AdvancedBaseModel):
|
class ContestFile(AdvancedBaseModel):
|
||||||
__tablename__ = 'contest_files'
|
__tablename__ = 'contest_files'
|
||||||
|
|
||||||
|
filename = Column(String, nullable=False)
|
||||||
file_path = Column(String, nullable=False)
|
file_path = Column(String, nullable=False)
|
||||||
|
|
||||||
contest_id = Column(Integer, ForeignKey('contests.id'), nullable=False)
|
contest_id = Column(Integer, ForeignKey('contests.id'), nullable=False)
|
||||||
|
|||||||
@ -29,8 +29,8 @@ class ContestCarouselPhotosService:
|
|||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
photo.file_path,
|
photo.file_path,
|
||||||
media_type=self.get_media_type(photo.file_path), # Use file_path to infer type
|
media_type=self.get_media_type(photo.file_path),
|
||||||
filename=os.path.basename(photo.file_path), # Extract filename from path
|
filename=os.path.basename(photo.file_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_photos_by_contest_id(self, contest_id: int) -> list[ContestCarouselPhotoEntity]:
|
async def get_photos_by_contest_id(self, contest_id: int) -> list[ContestCarouselPhotoEntity]:
|
||||||
@ -40,7 +40,7 @@ class ContestCarouselPhotosService:
|
|||||||
for photo in photos
|
for photo in photos
|
||||||
]
|
]
|
||||||
|
|
||||||
async def upload_photo(self, contest_id: int, file: UploadFile): # Removed 'user: User' for simplicity, add if needed
|
async def upload_photo(self, contest_id: int, file: UploadFile):
|
||||||
self.validate_file_type(file)
|
self.validate_file_type(file)
|
||||||
|
|
||||||
filename = self.generate_filename(file)
|
filename = self.generate_filename(file)
|
||||||
@ -89,8 +89,7 @@ class ContestCarouselPhotosService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_filename(file: UploadFile):
|
def generate_filename(file: UploadFile):
|
||||||
original_filename = secure_filename(file.filename)
|
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||||
return f"{uuid.uuid4()}_{original_filename}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def model_to_entity(photo_model: ContestCarouselPhoto) -> ContestCarouselPhotoEntity:
|
def model_to_entity(photo_model: ContestCarouselPhoto) -> ContestCarouselPhotoEntity:
|
||||||
|
|||||||
134
API/app/infrastructure/contest_files_service.py
Normal file
134
API/app/infrastructure/contest_files_service.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import magic
|
||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from app.application.contest_files_repository import ContestFilesRepository
|
||||||
|
from app.application.contests_repository import ContestsRepository
|
||||||
|
from app.domain.entities.contest_file import ContestFileEntity
|
||||||
|
from app.domain.models import ContestFile, User
|
||||||
|
|
||||||
|
|
||||||
|
class ContestFilesService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.contest_files_repository = ContestFilesRepository(db)
|
||||||
|
self.contests_repository = ContestsRepository(db)
|
||||||
|
|
||||||
|
async def get_file_by_id(self, file_id: int) -> FileResponse:
|
||||||
|
contest_file = await self.contest_files_repository.get_by_id(file_id)
|
||||||
|
|
||||||
|
if not contest_file:
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
if not os.path.exists(contest_file.file_path):
|
||||||
|
raise HTTPException(404, "Файл не найден на диске")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
contest_file.file_path,
|
||||||
|
media_type=self.get_media_type(contest_file.file_path),
|
||||||
|
filename=os.path.basename(contest_file.file_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_files_by_contest_id(self, contest_id: int) -> list[ContestFileEntity]:
|
||||||
|
files = await self.contest_files_repository.get_by_contest_id(contest_id)
|
||||||
|
return [self.model_to_entity(file) for file in files]
|
||||||
|
|
||||||
|
async def upload_file(self, contest_id: int, file: UploadFile, user: User) -> ContestFileEntity:
|
||||||
|
contest = await self.contests_repository.get_by_id(contest_id)
|
||||||
|
if not contest:
|
||||||
|
raise HTTPException(404, "Конкурс не найден")
|
||||||
|
|
||||||
|
self.validate_file_type(file)
|
||||||
|
file_path = await self.save_file(file)
|
||||||
|
|
||||||
|
contest_file = ContestFile(
|
||||||
|
filename=file.filename,
|
||||||
|
file_path=file_path,
|
||||||
|
contest_id=contest_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.model_to_entity(
|
||||||
|
await self.contest_files_repository.create(contest_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_file(self, file_id: int, user: User) -> ContestFileEntity:
|
||||||
|
contest_file = await self.contest_files_repository.get_by_id(file_id)
|
||||||
|
if not contest_file:
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
if os.path.exists(contest_file.file_path):
|
||||||
|
os.remove(contest_file.file_path)
|
||||||
|
|
||||||
|
return self.model_to_entity(
|
||||||
|
await self.contest_files_repository.delete(contest_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def save_file(self, file: UploadFile, upload_dir: str = "uploads/contest_files") -> 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)
|
||||||
|
|
||||||
|
allowed_types = [
|
||||||
|
"application/pdf",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"application/zip",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"text/plain",
|
||||||
|
]
|
||||||
|
|
||||||
|
if file_type not in allowed_types:
|
||||||
|
raise HTTPException(400, f"Недопустимый тип файла: {file_type}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_filename(file: UploadFile) -> str:
|
||||||
|
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def model_to_entity(contest_file_model: ContestFile) -> ContestFileEntity:
|
||||||
|
return ContestFileEntity(
|
||||||
|
id=contest_file_model.id,
|
||||||
|
filename=contest_file_model.filename,
|
||||||
|
file_path=contest_file_model.file_path,
|
||||||
|
contest_id=contest_file_model.contest_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_media_type(filename: str) -> str:
|
||||||
|
extension = filename.split('.')[-1].lower()
|
||||||
|
if extension in ['jpeg', 'jpg', 'png']:
|
||||||
|
return f"image/{extension}"
|
||||||
|
if extension == 'pdf':
|
||||||
|
return "application/pdf"
|
||||||
|
if extension in ['zip']:
|
||||||
|
return "application/zip"
|
||||||
|
if extension in ['doc', 'docx']:
|
||||||
|
return "application/msword"
|
||||||
|
if extension in ['xls', 'xlsx']:
|
||||||
|
return "application/vnd.ms-excel"
|
||||||
|
if extension in ['ppt', 'pptx']:
|
||||||
|
return "application/vnd.ms-powerpoint"
|
||||||
|
if extension in ['txt']:
|
||||||
|
return "text/plain"
|
||||||
|
return "application/octet-stream"
|
||||||
@ -13,6 +13,7 @@ from app.contollers.teams_router import router as team_router
|
|||||||
from app.contollers.users_router import router as users_router
|
from app.contollers.users_router import router as users_router
|
||||||
from app.contollers.contests_router import router as contest_router
|
from app.contollers.contests_router import router as contest_router
|
||||||
from app.contollers.contest_carousel_photos_router import router as contest_carousel_photos_router
|
from app.contollers.contest_carousel_photos_router import router as contest_carousel_photos_router
|
||||||
|
from app.contollers.contest_files_router import router as contest_files_router
|
||||||
from app.settings import settings
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ def start_app():
|
|||||||
api_app.include_router(contest_router, prefix=f'{settings.PREFIX}/contests', tags=['contests'])
|
api_app.include_router(contest_router, prefix=f'{settings.PREFIX}/contests', tags=['contests'])
|
||||||
api_app.include_router(contest_carousel_photos_router, prefix=f'{settings.PREFIX}/contest_carousel_photos',
|
api_app.include_router(contest_carousel_photos_router, prefix=f'{settings.PREFIX}/contest_carousel_photos',
|
||||||
tags=['contest_carousel_photos'])
|
tags=['contest_carousel_photos'])
|
||||||
|
api_app.include_router(contest_files_router, prefix=f'{settings.PREFIX}/contest_files', tags=['contest_files'])
|
||||||
|
|
||||||
return api_app
|
return api_app
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const deleteContestCarouselPhoto = async (photoId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/`, // Изменено здесь
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка удаления фотографии карусели конкурса с ID ${photoId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось удалить фотографию карусели конкурса: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deleteContestCarouselPhoto
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const downloadContestCarouselPhotoFile = async (photoId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
responseType: 'blob' // Важно для загрузки файлов
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка загрузки файла фотографии карусели конкурса с ID ${photoId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось загрузить файл фотографии карусели конкурса: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default downloadContestCarouselPhotoFile
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const getContestCarouselPhotosByContestId = async (contestId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка получения фотографий карусели конкурса для конкурса ${contestId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось загрузить фотографии карусели конкурса: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getContestCarouselPhotosByContestId
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const uploadContestCarouselPhoto = async (contestId, file) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
formData.append('contest_id', contestId);
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка загрузки фотографии в карусель конкурса с ID ${contestId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось загрузить фотографию в карусель конкурса: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uploadContestCarouselPhoto
|
||||||
31
WEB/src/api/contests/contest_files/deleteContestFile.js
Normal file
31
WEB/src/api/contests/contest_files/deleteContestFile.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const deleteContestFile = async (fileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token'); // Получаем токен из localStorage
|
||||||
|
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/contest_files/${fileId}/`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
console.error(`Ошибка удаления файла конкурса с ID ${fileId}:`, errorMessage);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Недостаточно прав для удаления файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Файл не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteContestFile;
|
||||||
39
WEB/src/api/contests/contest_files/downloadContestFile.js
Normal file
39
WEB/src/api/contests/contest_files/downloadContestFile.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const downloadContestFile = async (fileId) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/${fileId}/file`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
const contentDisposition = response.headers['content-disposition'];
|
||||||
|
let filename = 'download';
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
filename = filenameMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return filename;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа для скачивания файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Файл не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default downloadContestFile;
|
||||||
21
WEB/src/api/contests/contest_files/getContestFiles.js
Normal file
21
WEB/src/api/contests/contest_files/getContestFiles.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const getContestFiles = async (contestId) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/contests/${contestId}/`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к файлам конкурса (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Конкурс не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getContestFiles;
|
||||||
38
WEB/src/api/contests/contest_files/uploadContestFile.js
Normal file
38
WEB/src/api/contests/contest_files/uploadContestFile.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const uploadContestFile = async (contestId, file) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/contest_files/contests/${contestId}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Недостаточно прав для загрузки файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
throw new Error(`Ошибка загрузки: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Конкурс не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uploadContestFile;
|
||||||
30
WEB/src/api/projects/project_files/deleteProjectFile.js
Normal file
30
WEB/src/api/projects/project_files/deleteProjectFile.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const deleteProjectFile = async (fileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/project_files/${fileId}/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
console.error(`Ошибка удаления файла проекта с ID ${fileId}:`, errorMessage);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Недостаточно прав для удаления файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Файл не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteProjectFile;
|
||||||
51
WEB/src/api/projects/project_files/downloadProjectFile.js
Normal file
51
WEB/src/api/projects/project_files/downloadProjectFile.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const downloadProjectFile = async (fileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/project_files/${fileId}/download`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Создаем ссылку для скачивания
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const contentDisposition = response.headers['content-disposition'];
|
||||||
|
let filename = `project_file_${fileId}`;
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
filename = decodeURIComponent(filenameMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return { success: true, filename: filename };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
console.error(`Ошибка скачивания файла проекта с ID ${fileId}:`, errorMessage);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Недостаточно прав для скачивания файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Файл не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default downloadProjectFile;
|
||||||
24
WEB/src/api/projects/project_files/getProjectFiles.js
Normal file
24
WEB/src/api/projects/project_files/getProjectFiles.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const getProjectFiles = async (projectId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/project_files/projects/${projectId}/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
console.error(`Ошибка загрузки файлов проекта с ID ${projectId}:`, errorMessage);
|
||||||
|
throw new Error(`Не удалось загрузить файлы проекта: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getProjectFiles;
|
||||||
39
WEB/src/api/projects/project_files/uploadProjectFile.js
Normal file
39
WEB/src/api/projects/project_files/uploadProjectFile.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const uploadProjectFile = async (projectId, file) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/project_files/projects/${projectId}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message;
|
||||||
|
console.error(`Ошибка загрузки файла проекта с ID ${projectId}:`, errorMessage);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Недостаточно прав для загрузки файла (401)");
|
||||||
|
}
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
throw new Error(`Ошибка загрузки: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
throw new Error("Проект не найден (404)");
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uploadProjectFile;
|
||||||
@ -168,7 +168,7 @@
|
|||||||
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
||||||
<q-card v-for="photo in profilePhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
|
<q-card v-for="photo in profilePhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
|
||||||
<q-img
|
<q-img
|
||||||
:src="getPhotoUrl(photo.id)"
|
:src="getPhotoUrl(photo.id, 'profile')"
|
||||||
alt="Profile Photo"
|
alt="Profile Photo"
|
||||||
style="width: 100%; height: 100%; object-fit: cover;"
|
style="width: 100%; height: 100%; object-fit: cover;"
|
||||||
>
|
>
|
||||||
@ -179,7 +179,7 @@
|
|||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="confirmDeletePhoto(photo.id)"
|
@click="confirmDeletePhoto(photo.id, 'profile')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-img>
|
</q-img>
|
||||||
@ -206,7 +206,7 @@
|
|||||||
label="Загрузить фото"
|
label="Загрузить фото"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-mt-sm full-width"
|
class="q-mt-sm full-width"
|
||||||
@click="uploadNewPhoto"
|
@click="uploadNewProfilePhoto"
|
||||||
:loading="uploadingPhoto"
|
:loading="uploadingPhoto"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -221,11 +221,127 @@
|
|||||||
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
|
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
|
||||||
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.photo" label="Фото" dense clearable class="q-mt-sm" />
|
|
||||||
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
||||||
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.project_id" label="Проект" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.project_id" label="Проект" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
||||||
|
|
||||||
|
<q-separator class="q-my-md" />
|
||||||
|
<div class="text-h6 q-mb-sm">Фотографии карусели конкурса</div>
|
||||||
|
|
||||||
|
<div v-if="loadingContestPhotos" class="text-center q-py-md">
|
||||||
|
<q-spinner-dots color="primary" size="2em" />
|
||||||
|
<div>Загрузка фотографий...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="contestPhotos.length === 0" class="text-center q-py-md text-grey-7">
|
||||||
|
Пока нет фотографий.
|
||||||
|
</div>
|
||||||
|
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
||||||
|
<q-card v-for="photo in contestPhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
|
||||||
|
<q-img
|
||||||
|
:src="getPhotoUrl(photo.id, 'contest')"
|
||||||
|
alt="Contest Photo"
|
||||||
|
style="width: 100%; height: 100%; object-fit: cover;"
|
||||||
|
>
|
||||||
|
<div class="absolute-bottom text-right q-pa-xs">
|
||||||
|
<q-btn
|
||||||
|
icon="delete"
|
||||||
|
color="negative"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
@click="confirmDeletePhoto(photo.id, 'contest')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-file
|
||||||
|
v-model="newContestPhotoFile"
|
||||||
|
label="Выберите фото для загрузки"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
accept="image/*"
|
||||||
|
@update:model-value="handleNewContestPhotoSelected"
|
||||||
|
class="q-mt-sm"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="newContestPhotoFile" name="check" color="positive" />
|
||||||
|
<q-icon name="photo" />
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
<q-btn
|
||||||
|
v-if="newContestPhotoFile"
|
||||||
|
label="Загрузить фото"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-sm full-width"
|
||||||
|
@click="uploadNewContestPhoto"
|
||||||
|
:loading="uploadingPhoto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-separator class="q-my-md" />
|
||||||
|
<div class="text-h6 q-mb-sm">Файлы конкурса</div>
|
||||||
|
|
||||||
|
<div v-if="loadingContestFiles" class="text-center q-py-md">
|
||||||
|
<q-spinner-dots color="primary" size="2em" />
|
||||||
|
<div>Загрузка файлов...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="contestFiles.length === 0" class="text-center q-py-md text-grey-7">
|
||||||
|
Пока нет файлов.
|
||||||
|
</div>
|
||||||
|
<q-list v-else bordered separator class="rounded-borders">
|
||||||
|
<q-item v-for="fileItem in contestFiles" :key="fileItem.id" clickable v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ fileItem.filename }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ fileItem.file_path }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<div class="q-gutter-xs">
|
||||||
|
<q-btn
|
||||||
|
icon="download"
|
||||||
|
color="primary"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
@click.stop="downloadExistingContestFile(fileItem.id)"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
icon="delete"
|
||||||
|
color="negative"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
@click.stop="confirmDeleteContestFile(fileItem.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<q-file
|
||||||
|
v-model="newContestFile"
|
||||||
|
label="Выберите файл для загрузки"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
@update:model-value="handleNewContestFileSelected"
|
||||||
|
class="q-mt-sm"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="newContestFile" name="check" color="positive" />
|
||||||
|
<q-icon name="attach_file" />
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
<q-btn
|
||||||
|
v-if="newContestFile"
|
||||||
|
label="Загрузить файл"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-sm full-width"
|
||||||
|
@click="uploadNewContestFile"
|
||||||
|
:loading="uploadingFile"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -289,9 +405,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { Notify, useQuasar } from 'quasar' // Импортируем useQuasar для QDialog
|
import { Notify, useQuasar } from 'quasar'
|
||||||
|
|
||||||
// Импорты API для управления данными
|
|
||||||
import fetchTeams from '@/api/teams/getTeams.js'
|
import fetchTeams from '@/api/teams/getTeams.js'
|
||||||
import updateTeam from '@/api/teams/updateTeam.js'
|
import updateTeam from '@/api/teams/updateTeam.js'
|
||||||
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
||||||
@ -312,17 +427,33 @@ import updateContest from '@/api/contests/updateContest.js'
|
|||||||
import deleteContestById from '@/api/contests/deleteContest.js'
|
import deleteContestById from '@/api/contests/deleteContest.js'
|
||||||
import createContest from '@/api/contests/createContest.js'
|
import createContest from '@/api/contests/createContest.js'
|
||||||
|
|
||||||
import router from "@/router/index.js"
|
import CONFIG from '@/core/config.js'
|
||||||
import CONFIG from '@/core/config.js' // Убедитесь, что у вас есть этот файл конфигурации
|
|
||||||
|
|
||||||
// --- Импорты для работы с фотографиями профиля ---
|
import getProfilePhotosByProfileId from '@/api/profiles/profile_photos/getPhotoFileById.js'
|
||||||
import getPhotosByProfileId from '@/api/profiles/profile_photos/getPhotoFileById.js'
|
|
||||||
import uploadProfilePhoto from '@/api/profiles/profile_photos/uploadProfilePhoto.js'
|
import uploadProfilePhoto from '@/api/profiles/profile_photos/uploadProfilePhoto.js'
|
||||||
import deletePhoto from '@/api/profiles/profile_photos/deletePhoto.js'
|
import deleteProfilePhoto from '@/api/profiles/profile_photos/deletePhoto.js'
|
||||||
|
import downloadProfilePhotoFile from '@/api/profiles/profile_photos/downloadPhotoFile.js'
|
||||||
|
|
||||||
|
import getContestCarouselPhotosByContestId from '@/api/contests/contest_carousel_photos/getContestPhotoFileById.js'
|
||||||
|
import uploadContestCarouselPhoto from '@/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js'
|
||||||
|
import deleteContestCarouselPhoto from '@/api/contests/contest_carousel_photos/deleteContestPhoto.js'
|
||||||
|
import downloadContestCarouselPhotoFile from '@/api/contests/contest_carousel_photos/downloadContestPhotoFile.js'
|
||||||
|
|
||||||
|
// --- Imports for Contest Files ---
|
||||||
|
import getContestFiles from '@/api/contests/contest_files/getContestFiles.js'
|
||||||
|
import uploadContestFile from '@/api/contests/contest_files/uploadContestFile.js'
|
||||||
|
import deleteContestFile from '@/api/contests/contest_files/deleteContestFile.js'
|
||||||
|
import downloadContestFile from '@/api/contests/contest_files/downloadContestFile.js'
|
||||||
|
|
||||||
|
// --- Imports for Project Files ---
|
||||||
|
import getProjectFiles from '@/api/projects/project_files/getProjectFiles.js'
|
||||||
|
import uploadProjectFile from '@/api/projects/project_files/uploadProjectFile.js'
|
||||||
|
import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js'
|
||||||
|
import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js'
|
||||||
|
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
|
|
||||||
// Текущая вкладка
|
|
||||||
const tab = ref('profiles')
|
const tab = ref('profiles')
|
||||||
|
|
||||||
// --- Profiles ---
|
// --- Profiles ---
|
||||||
@ -362,9 +493,9 @@ const projectColumns = [
|
|||||||
const contests = ref([])
|
const contests = ref([])
|
||||||
const loadingContests = ref(false)
|
const loadingContests = ref(false)
|
||||||
const contestColumns = [
|
const contestColumns = [
|
||||||
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
||||||
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||||
{ name: 'web_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
{ name: 'web_url', label: 'URL сайта', field: 'web_url', sortable: true },
|
||||||
{ name: 'photo', label: 'Фото', field: 'photo', sortable: true },
|
{ name: 'photo', label: 'Фото', field: 'photo', sortable: true },
|
||||||
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
||||||
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
||||||
@ -381,18 +512,37 @@ const dialogType = ref('')
|
|||||||
const profilePhotos = ref([])
|
const profilePhotos = ref([])
|
||||||
const loadingProfilePhotos = ref(false)
|
const loadingProfilePhotos = ref(false)
|
||||||
const newProfilePhotoFile = ref(null)
|
const newProfilePhotoFile = ref(null)
|
||||||
const uploadingPhoto = ref(false)
|
const uploadingPhoto = ref(false) // Общее состояние для загрузки фото
|
||||||
|
|
||||||
|
// --- Состояния для фотографий карусели конкурса ---
|
||||||
|
const contestPhotos = ref([])
|
||||||
|
const loadingContestPhotos = ref(false)
|
||||||
|
const newContestPhotoFile = ref(null)
|
||||||
|
|
||||||
|
// --- Состояния для файлов конкурса ---
|
||||||
|
const contestFiles = ref([])
|
||||||
|
const loadingContestFiles = ref(false)
|
||||||
|
const newContestFile = ref(null)
|
||||||
|
const uploadingFile = ref(false) // Отдельное состояние для загрузки файлов
|
||||||
|
|
||||||
|
|
||||||
// Функция для получения URL фото
|
// Функция для получения URL фото
|
||||||
const getPhotoUrl = (photoId) => {
|
const getPhotoUrl = (photoId, type) => {
|
||||||
|
if (type === 'profile') {
|
||||||
|
// Используем шаблонные строки JavaScript для корректного формирования URL
|
||||||
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
||||||
|
} else if (type === 'contest') {
|
||||||
|
// Используем шаблонные строки JavaScript для корректного формирования URL
|
||||||
|
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка фотографий профиля при открытии диалога для профиля
|
// Загрузка фотографий профиля при открытии диалога для профиля
|
||||||
async function loadProfilePhotos(profileId) {
|
async function loadProfilePhotos(profileId) {
|
||||||
loadingProfilePhotos.value = true;
|
loadingProfilePhotos.value = true;
|
||||||
try {
|
try {
|
||||||
profilePhotos.value = await getPhotosByProfileId(profileId);
|
profilePhotos.value = await getProfilePhotosByProfileId(profileId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
@ -405,13 +555,58 @@ async function loadProfilePhotos(profileId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчик выбора нового файла
|
// Загрузка фотографий карусели конкурса
|
||||||
|
async function loadContestPhotos(contestId) {
|
||||||
|
loadingContestPhotos.value = true;
|
||||||
|
try {
|
||||||
|
contestPhotos.value = await getContestCarouselPhotosByContestId(contestId);
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки фотографий карусели конкурса: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
contestPhotos.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingContestPhotos.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка файлов конкурса
|
||||||
|
async function loadContestFiles(contestId) {
|
||||||
|
loadingContestFiles.value = true;
|
||||||
|
try {
|
||||||
|
contestFiles.value = await getContestFiles(contestId);
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки файлов конкурса: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
contestFiles.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingContestFiles.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик выбора нового файла для профиля
|
||||||
function handleNewPhotoSelected(file) {
|
function handleNewPhotoSelected(file) {
|
||||||
newProfilePhotoFile.value = file;
|
newProfilePhotoFile.value = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка новой фотографии
|
// Обработчик выбора нового файла для конкурса (фото)
|
||||||
async function uploadNewPhoto() {
|
function handleNewContestPhotoSelected(file) {
|
||||||
|
newContestPhotoFile.value = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик выбора нового файла для конкурса (обычный файл)
|
||||||
|
function handleNewContestFileSelected(file) {
|
||||||
|
newContestFile.value = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Загрузка новой фотографии профиля
|
||||||
|
async function uploadNewProfilePhoto() {
|
||||||
if (!newProfilePhotoFile.value || !dialogData.value.id) {
|
if (!newProfilePhotoFile.value || !dialogData.value.id) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -424,18 +619,17 @@ async function uploadNewPhoto() {
|
|||||||
uploadingPhoto.value = true;
|
uploadingPhoto.value = true;
|
||||||
try {
|
try {
|
||||||
const uploadedPhoto = await uploadProfilePhoto(dialogData.value.id, newProfilePhotoFile.value);
|
const uploadedPhoto = await uploadProfilePhoto(dialogData.value.id, newProfilePhotoFile.value);
|
||||||
// Добавляем новую фото в список
|
|
||||||
profilePhotos.value.push(uploadedPhoto);
|
profilePhotos.value.push(uploadedPhoto);
|
||||||
newProfilePhotoFile.value = null; // Сбрасываем выбранный файл
|
newProfilePhotoFile.value = null;
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Фотография успешно загружена!',
|
message: 'Фотография профиля успешно загружена!',
|
||||||
icon: 'check_circle',
|
icon: 'check_circle',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: `Ошибка загрузки фотографии: ${error.message}`,
|
message: `Ошибка загрузки фотографии профиля: ${error.message}`,
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -443,8 +637,73 @@ async function uploadNewPhoto() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загрузка новой фотографии карусели конкурса
|
||||||
|
async function uploadNewContestPhoto() {
|
||||||
|
if (!newContestPhotoFile.value || !dialogData.value.id) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Выберите файл и убедитесь, что конкурс выбран.',
|
||||||
|
icon: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadingPhoto.value = true;
|
||||||
|
try {
|
||||||
|
const uploadedPhoto = await uploadContestCarouselPhoto(dialogData.value.id, newContestPhotoFile.value);
|
||||||
|
contestPhotos.value.push(uploadedPhoto);
|
||||||
|
newContestPhotoFile.value = null;
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Фотография карусели конкурса успешно загружена!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки фотографии карусели конкурса: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
uploadingPhoto.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка нового файла конкурса
|
||||||
|
async function uploadNewContestFile() {
|
||||||
|
if (!newContestFile.value || !dialogData.value.id) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Выберите файл и убедитесь, что конкурс выбран.',
|
||||||
|
icon: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadingFile.value = true; // Use separate loading state
|
||||||
|
try {
|
||||||
|
const uploadedFile = await uploadContestFile(dialogData.value.id, newContestFile.value);
|
||||||
|
contestFiles.value.push(uploadedFile);
|
||||||
|
newContestFile.value = null;
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Файл конкурса успешно загружен!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки файла конкурса: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
uploadingFile.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Подтверждение и удаление фотографии
|
// Подтверждение и удаление фотографии
|
||||||
function confirmDeletePhoto(photoId) {
|
function confirmDeletePhoto(photoId, type) {
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: 'Подтверждение удаления',
|
title: 'Подтверждение удаления',
|
||||||
message: 'Вы уверены, что хотите удалить эту фотографию?',
|
message: 'Вы уверены, что хотите удалить эту фотографию?',
|
||||||
@ -459,23 +718,89 @@ function confirmDeletePhoto(photoId) {
|
|||||||
color: 'primary'
|
color: 'primary'
|
||||||
}
|
}
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
await deleteExistingPhoto(photoId);
|
await deleteExistingPhoto(photoId, type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteExistingPhoto(photoId) {
|
async function deleteExistingPhoto(photoId, type) {
|
||||||
try {
|
try {
|
||||||
await deletePhoto(photoId);
|
if (type === 'profile') {
|
||||||
|
await deleteProfilePhoto(photoId);
|
||||||
profilePhotos.value = profilePhotos.value.filter(p => p.id !== photoId);
|
profilePhotos.value = profilePhotos.value.filter(p => p.id !== photoId);
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Фотография успешно удалена!',
|
message: 'Фотография профиля успешно удалена!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
} else if (type === 'contest') {
|
||||||
|
await deleteContestCarouselPhoto(photoId);
|
||||||
|
contestPhotos.value = contestPhotos.value.filter(p => p.id !== photoId);
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Фотография карусели конкурса успешно удалена!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка удаления фотографии: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подтверждение и удаление файла конкурса
|
||||||
|
function confirmDeleteContestFile(fileId) {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Подтверждение удаления',
|
||||||
|
message: 'Вы уверены, что хотите удалить этот файл?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true,
|
||||||
|
ok: {
|
||||||
|
label: 'Удалить',
|
||||||
|
color: 'negative'
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: 'Отмена',
|
||||||
|
color: 'primary'
|
||||||
|
}
|
||||||
|
}).onOk(async () => {
|
||||||
|
await deleteExistingContestFile(fileId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteExistingContestFile(fileId) {
|
||||||
|
try {
|
||||||
|
await deleteContestFile(fileId);
|
||||||
|
contestFiles.value = contestFiles.value.filter(f => f.id !== fileId);
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Файл конкурса успешно удален!',
|
||||||
icon: 'check_circle',
|
icon: 'check_circle',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: `Ошибка удаления фотографии: ${error.message}`,
|
message: `Ошибка удаления файла конкурса: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скачивание файла конкурса
|
||||||
|
async function downloadExistingContestFile(fileId) {
|
||||||
|
try {
|
||||||
|
await downloadContestFile(fileId);
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Файл конкурса успешно скачан!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка скачивания файла конкурса: ${error.message}`,
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -490,20 +815,27 @@ function openEdit(type, row) {
|
|||||||
if (type === 'teams') {
|
if (type === 'teams') {
|
||||||
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
|
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
|
||||||
} else if (type === 'projects') {
|
} else if (type === 'projects') {
|
||||||
dialogData.value = { name: '', summary: '', deadline: '' }
|
dialogData.value = { title: '', description: '', repository_url: '' }
|
||||||
} else if (type === 'profiles') {
|
} else if (type === 'profiles') {
|
||||||
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null }
|
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null }
|
||||||
profilePhotos.value = [];
|
profilePhotos.value = [];
|
||||||
} else if (type === 'contests') {
|
} else if (type === 'contests') {
|
||||||
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null }
|
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null }
|
||||||
|
contestPhotos.value = [];
|
||||||
|
contestFiles.value = []; // Clear contest files when opening dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
|
|
||||||
if (type === 'profiles' && dialogData.value.id) {
|
if (type === 'profiles' && dialogData.value.id) {
|
||||||
loadProfilePhotos(dialogData.value.id);
|
loadProfilePhotos(dialogData.value.id);
|
||||||
|
} else if (type === 'contests' && dialogData.value.id) {
|
||||||
|
loadContestPhotos(dialogData.value.id);
|
||||||
|
loadContestFiles(dialogData.value.id); // Load contest files
|
||||||
} else {
|
} else {
|
||||||
profilePhotos.value = [];
|
profilePhotos.value = [];
|
||||||
|
contestPhotos.value = [];
|
||||||
|
contestFiles.value = []; // Clear contest files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,7 +847,12 @@ function closeDialog() {
|
|||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
profilePhotos.value = [];
|
profilePhotos.value = [];
|
||||||
newProfilePhotoFile.value = null;
|
newProfilePhotoFile.value = null;
|
||||||
|
contestPhotos.value = [];
|
||||||
|
newContestPhotoFile.value = null;
|
||||||
|
contestFiles.value = []; // Clear contest files
|
||||||
|
newContestFile.value = null; // Clear new contest file input
|
||||||
uploadingPhoto.value = false;
|
uploadingPhoto.value = false;
|
||||||
|
uploadingFile.value = false; // Reset file uploading state
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveChanges() {
|
async function saveChanges() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user