добавил методы для конкурсов
This commit is contained in:
parent
00d17363f2
commit
203f6763d6
16
API/app/application/contest_statuses_repository.py
Normal file
16
API/app/application/contest_statuses_repository.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Contest
|
||||||
|
|
||||||
|
|
||||||
|
class ContestStatusesRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_by_id(self, contest_status_id: int) -> Optional[Contest]:
|
||||||
|
stmt = select(Contest).filter_by(id=contest_status_id)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().first()
|
||||||
37
API/app/application/contests_repository.py
Normal file
37
API/app/application/contests_repository.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select, Sequence
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Contest
|
||||||
|
|
||||||
|
|
||||||
|
class ContestsRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_all(self) -> Sequence[Contest]:
|
||||||
|
stmt = select(Contest)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def get_by_id(self, contest_id: int) -> Optional[Contest]:
|
||||||
|
stmt = select(Contest).filter_by(id=contest_id)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def create(self, contest: Contest) -> Contest:
|
||||||
|
self.db.add(contest)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(contest)
|
||||||
|
return contest
|
||||||
|
|
||||||
|
async def update(self, contest: Contest) -> Contest:
|
||||||
|
await self.db.merge(contest)
|
||||||
|
await self.db.commit()
|
||||||
|
return contest
|
||||||
|
|
||||||
|
async def delete(self, contest: Contest) -> Contest:
|
||||||
|
await self.db.delete(contest)
|
||||||
|
await self.db.commit()
|
||||||
|
return contest
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, Sequence
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.domain.models import Profile
|
from app.domain.models import Profile
|
||||||
@ -10,6 +10,11 @@ class ProfilesRepository:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
|
async def get_all(self) -> Sequence[Profile]:
|
||||||
|
stmt = select(Profile)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
async def get_by_id(self, profile_id: int) -> Optional[Profile]:
|
async def get_by_id(self, profile_id: int) -> Optional[Profile]:
|
||||||
stmt = select(Profile).filter_by(id=profile_id)
|
stmt = select(Profile).filter_by(id=profile_id)
|
||||||
result = await self.db.execute(stmt)
|
result = await self.db.execute(stmt)
|
||||||
|
|||||||
88
API/app/contollers/contests_router.py
Normal file
88
API/app/contollers/contests_router.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database.session import get_db
|
||||||
|
from app.domain.entities.contest import ContestEntity
|
||||||
|
from app.infrastructure.contests_service import ContestsService
|
||||||
|
from app.infrastructure.dependencies import require_admin, get_current_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/',
|
||||||
|
response_model=list[ContestEntity],
|
||||||
|
summary='Get all contests',
|
||||||
|
description='Returns all contests',
|
||||||
|
)
|
||||||
|
async def get_all_contests(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
contests_service = ContestsService(db)
|
||||||
|
return await contests_service.get_all_contests()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/',
|
||||||
|
response_model=Optional[ContestEntity],
|
||||||
|
summary='Create a new contest',
|
||||||
|
description='Creates a new contest',
|
||||||
|
)
|
||||||
|
async def create_contest(
|
||||||
|
contest: ContestEntity,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
contests_service = ContestsService(db)
|
||||||
|
return await contests_service.create_contest(contest)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
'/{contest_id}/',
|
||||||
|
response_model=Optional[ContestEntity],
|
||||||
|
summary='Update a contest',
|
||||||
|
description='Updates a contest',
|
||||||
|
)
|
||||||
|
async def update_contest(
|
||||||
|
contest_id: int,
|
||||||
|
contest: ContestEntity,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
contests_service = ContestsService(db)
|
||||||
|
return await contests_service.update_contest(contest_id, contest, user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'/{contest_id}/',
|
||||||
|
response_model=Optional[ContestEntity],
|
||||||
|
summary='Delete a contest',
|
||||||
|
description='Delete a contest',
|
||||||
|
)
|
||||||
|
async def delete_contest(
|
||||||
|
contest_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
contests_service = ContestsService(db)
|
||||||
|
return await contests_service.delete(contest_id, user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/project/{project_id}/',
|
||||||
|
response_model=Optional[ContestEntity],
|
||||||
|
summary='Get project by contest ID',
|
||||||
|
description='Retrieve project data by contest ID',
|
||||||
|
)
|
||||||
|
async def get_project_by_contest_id(
|
||||||
|
project_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
contests_service = ContestsService(db)
|
||||||
|
contest = await contests_service.get_by_project(project_id)
|
||||||
|
|
||||||
|
return contest
|
||||||
|
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
@ -10,6 +10,18 @@ from app.infrastructure.profiles_service import ProfilesService
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/',
|
||||||
|
response_model=list[ProfileEntity],
|
||||||
|
summary='Get all profiles',
|
||||||
|
description='Returns all profiles',
|
||||||
|
)
|
||||||
|
async def get_all_profiles(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
profiles_service = ProfilesService(db)
|
||||||
|
return await profiles_service.get_all_profiles()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
13
API/app/domain/entities/contest.py
Normal file
13
API/app/domain/entities/contest.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class ContestEntity(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
web_url: str
|
||||||
|
photo: Optional[str] = None
|
||||||
|
results: Optional[str] = None
|
||||||
|
is_win: Optional[bool] = None
|
||||||
|
project_id: int
|
||||||
|
status_id: int
|
||||||
158
API/app/infrastructure/contests_service.py
Normal file
158
API/app/infrastructure/contests_service.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.application.contest_statuses_repository import ContestStatusesRepository
|
||||||
|
from app.application.contests_repository import ContestsRepository
|
||||||
|
from app.application.projects_repository import ProjectsRepository
|
||||||
|
from app.application.users_repository import UsersRepository
|
||||||
|
from app.domain.entities.contest import ContestEntity
|
||||||
|
from app.domain.models import Contest, User
|
||||||
|
|
||||||
|
|
||||||
|
class ContestsService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.contests_repository = ContestsRepository(db)
|
||||||
|
self.projects_repository = ProjectsRepository(db)
|
||||||
|
self.statuses_repository = ContestStatusesRepository(db)
|
||||||
|
self.users_repository = UsersRepository(db)
|
||||||
|
|
||||||
|
async def get_all_contests(self) -> list[ContestEntity]:
|
||||||
|
contests = await self.contests_repository.get_all()
|
||||||
|
return [
|
||||||
|
self.model_to_entity(contest)
|
||||||
|
for contest in contests
|
||||||
|
]
|
||||||
|
|
||||||
|
async def create_contest(self, contest: ContestEntity) -> Optional[ContestEntity]:
|
||||||
|
project = await self.projects_repository.get_by_id(contest.project_id)
|
||||||
|
if project is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='The project with this ID was not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
status_contest = await self.statuses_repository.get_by_id(contest.status_id)
|
||||||
|
if status_contest is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='The status with this ID was not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
contest_model = self.entity_to_model(contest)
|
||||||
|
|
||||||
|
contest_model = await self.contests_repository.create(contest_model)
|
||||||
|
|
||||||
|
return self.model_to_entity(contest_model)
|
||||||
|
|
||||||
|
async def update_contest(self, contest_id: int, contest: ContestEntity, user: User) -> Optional[
|
||||||
|
ContestEntity
|
||||||
|
]:
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
elif user.profile.role.title != 'Администратор':
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Permission denied",
|
||||||
|
)
|
||||||
|
|
||||||
|
contest_model = await self.contests_repository.get_by_id(contest_id)
|
||||||
|
if contest_model is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='The contest with this ID was not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
project = await self.projects_repository.get_by_id(contest.project_id)
|
||||||
|
if project is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='The project with this ID was not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
status_contest = await self.statuses_repository.get_by_id(contest.role_id)
|
||||||
|
if status_contest is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail='The status with this ID was not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
contest_model.title = contest.title
|
||||||
|
contest_model.description = contest.description
|
||||||
|
contest_model.web_url = contest.web_url
|
||||||
|
contest_model.photo = contest.photo
|
||||||
|
contest_model.results = contest.results
|
||||||
|
contest_model.is_win = contest.is_win
|
||||||
|
contest_model.project_id = contest.project_id
|
||||||
|
contest_model.status_id = contest.status_id
|
||||||
|
|
||||||
|
contest_model = await self.contests_repository.update(contest_model)
|
||||||
|
|
||||||
|
return self.model_to_entity(contest_model)
|
||||||
|
|
||||||
|
async def delete(self, contest_id: int, user: User):
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
contest_model = await self.contests_repository.get_by_id(contest_id)
|
||||||
|
if user.profile.role.title != 'Администратор':
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail='Permission denied',
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.contests_repository.delete(contest_model)
|
||||||
|
|
||||||
|
return self.model_to_entity(result)
|
||||||
|
|
||||||
|
async def get_by_project(self, project_id: int) -> Optional[ContestEntity]:
|
||||||
|
project = await self.projects_repository.get_by_id(project_id)
|
||||||
|
if project is None:
|
||||||
|
raise HTTPException(status_code=404, detail='Project not found')
|
||||||
|
|
||||||
|
contest_model = await self.contests_repository.get_by_id(project.contest_id)
|
||||||
|
if not contest_model:
|
||||||
|
raise HTTPException(status_code=404, detail='Contest not found')
|
||||||
|
|
||||||
|
return self.model_to_entity(contest_model)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def model_to_entity(contest_model: Contest) -> ContestEntity:
|
||||||
|
return ContestEntity(
|
||||||
|
id=contest_model.id,
|
||||||
|
title=contest_model.title,
|
||||||
|
description=contest_model.description,
|
||||||
|
web_url=contest_model.web_url,
|
||||||
|
photo=contest_model.photo,
|
||||||
|
results=contest_model.results,
|
||||||
|
is_win=contest_model.is_win,
|
||||||
|
project_id=contest_model.project_id,
|
||||||
|
status_id=contest_model.status_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def entity_to_model(contest_entity: ContestEntity) -> Contest:
|
||||||
|
contest_model = Contest(
|
||||||
|
title=contest_entity.title,
|
||||||
|
description=contest_entity.description,
|
||||||
|
web_url=contest_entity.web_url,
|
||||||
|
photo=contest_entity.photo,
|
||||||
|
results=contest_entity.results,
|
||||||
|
is_win=contest_entity.is_win,
|
||||||
|
project_id=contest_entity.project_id,
|
||||||
|
status_id=contest_entity.status_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if contest_entity.id is not None:
|
||||||
|
contest_model.id = contest_entity.id
|
||||||
|
|
||||||
|
return contest_model
|
||||||
@ -18,6 +18,13 @@ class ProfilesService:
|
|||||||
self.roles_repository = RolesRepository(db)
|
self.roles_repository = RolesRepository(db)
|
||||||
self.users_repository = UsersRepository(db)
|
self.users_repository = UsersRepository(db)
|
||||||
|
|
||||||
|
async def get_all_profiles(self) -> list[ProfileEntity]:
|
||||||
|
profiles = await self.profiles_repository.get_all()
|
||||||
|
return [
|
||||||
|
self.model_to_entity(profile)
|
||||||
|
for profile in profiles
|
||||||
|
]
|
||||||
|
|
||||||
async def create_profile(self, profile: ProfileEntity) -> Optional[ProfileEntity]:
|
async def create_profile(self, profile: ProfileEntity) -> Optional[ProfileEntity]:
|
||||||
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:
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from app.contollers.register_router import router as register_router
|
|||||||
from app.contollers.rss_router import router as rss_router
|
from app.contollers.rss_router import router as rss_router
|
||||||
from app.contollers.teams_router import router as team_router
|
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.settings import settings
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ def start_app():
|
|||||||
api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
|
api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
|
||||||
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'])
|
||||||
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
|
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
|
||||||
|
api_app.include_router(contest_router,prefix=f'{settings.PREFIX}/contests', tags=['contests'])
|
||||||
|
|
||||||
return api_app
|
return api_app
|
||||||
|
|
||||||
|
|||||||
24
WEB/src/api/contests/createProfile.js
Normal file
24
WEB/src/api/contests/createProfile.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const createProfile = async (profile) => {
|
||||||
|
console.log(profile)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/profiles`,
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createProfile
|
||||||
22
WEB/src/api/contests/deleteProfile.js
Normal file
22
WEB/src/api/contests/deleteProfile.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const deleteProfile = async (profileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/profiles/${profileId}`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deleteProfile
|
||||||
25
WEB/src/api/contests/getProfiles.js
Normal file
25
WEB/src/api/contests/getProfiles.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '../../core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/profiles`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к пользователям");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchProfile;
|
||||||
31
WEB/src/api/contests/updateProfile.js
Normal file
31
WEB/src/api/contests/updateProfile.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const updateProfile = async (profile) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
// Убираем id из тела запроса, он идет в URL
|
||||||
|
const { id, ...profileData } = profile
|
||||||
|
|
||||||
|
console.log('Отправляем на сервер:', profileData)
|
||||||
|
|
||||||
|
const response = await axios.put(
|
||||||
|
`${CONFIG.BASE_URL}/profiles/${id}`,
|
||||||
|
profileData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Ответ от сервера:', response.data)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateProfile
|
||||||
24
WEB/src/api/profiles/createProfile.js
Normal file
24
WEB/src/api/profiles/createProfile.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const createProfile = async (profile) => {
|
||||||
|
console.log(profile)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/profiles`,
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createProfile
|
||||||
22
WEB/src/api/profiles/deleteProfile.js
Normal file
22
WEB/src/api/profiles/deleteProfile.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const deleteProfile = async (profileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/profiles/${profileId}`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default deleteProfile
|
||||||
25
WEB/src/api/profiles/getProfiles.js
Normal file
25
WEB/src/api/profiles/getProfiles.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '../../core/config.js'
|
||||||
|
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/profiles`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к пользователям");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchProfile;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import CONFIG from '../../core/config.js'
|
|
||||||
|
|
||||||
const getUserProfile = async (user_id, token) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/profiles/${user_id}/`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error.response?.data?.detail || 'Ошибка получения профиля')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getUserProfile
|
|
||||||
31
WEB/src/api/profiles/updateProfile.js
Normal file
31
WEB/src/api/profiles/updateProfile.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const updateProfile = async (profile) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
// Убираем id из тела запроса, он идет в URL
|
||||||
|
const { id, ...profileData } = profile
|
||||||
|
|
||||||
|
console.log('Отправляем на сервер:', profileData)
|
||||||
|
|
||||||
|
const response = await axios.put(
|
||||||
|
`${CONFIG.BASE_URL}/profiles/${id}`,
|
||||||
|
profileData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Ответ от сервера:', response.data)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateProfile
|
||||||
@ -2,6 +2,7 @@ import axios from 'axios'
|
|||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const createProject = async (project) => {
|
const createProject = async (project) => {
|
||||||
|
console.log(project)
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
|
|||||||
@ -8,7 +8,6 @@ const updateProject = async (project) => {
|
|||||||
// Убираем id из тела запроса, он идет в URL
|
// Убираем id из тела запроса, он идет в URL
|
||||||
const { id, ...projectData } = project
|
const { id, ...projectData } = project
|
||||||
|
|
||||||
console.log('Отправляем на сервер:', projectData)
|
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/projects/${id}`,
|
`${CONFIG.BASE_URL}/projects/${id}`,
|
||||||
@ -21,7 +20,6 @@ const updateProject = async (project) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('Ответ от сервера:', response.data)
|
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.detail || error.message)
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
|||||||
@ -8,7 +8,6 @@ const updateTeam = async (team) => {
|
|||||||
// Убираем id из тела запроса, он идет в URL
|
// Убираем id из тела запроса, он идет в URL
|
||||||
const { id, ...teamData } = team
|
const { id, ...teamData } = team
|
||||||
|
|
||||||
console.log('Отправляем на сервер:', teamData)
|
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/teams/${id}`,
|
`${CONFIG.BASE_URL}/teams/${id}`,
|
||||||
@ -21,7 +20,6 @@ const updateTeam = async (team) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('Ответ от сервера:', response.data)
|
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.detail || error.message)
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
animated
|
animated
|
||||||
@update:model-value="loadData"
|
@update:model-value="loadData"
|
||||||
>
|
>
|
||||||
<q-tab name="users" label="Пользователи" />
|
<q-tab name="profiles" label="Пользователи" />
|
||||||
<q-tab name="teams" label="Команды" />
|
<q-tab name="teams" label="Команды" />
|
||||||
<q-tab name="projects" label="Проекты" />
|
<q-tab name="projects" label="Проекты" />
|
||||||
<q-tab name="contests" label="Конкурсы" />
|
<q-tab name="contests" label="Конкурсы" />
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<q-tab-panels v-model="tab" animated transition-prev="slide-right" transition-next="slide-left">
|
<q-tab-panels v-model="tab" animated transition-prev="slide-right" transition-next="slide-left">
|
||||||
|
|
||||||
<!-- Пользователи -->
|
<!-- Пользователи -->
|
||||||
<q-tab-panel name="users">
|
<q-tab-panel name="profiles">
|
||||||
<div class="violet-card q-pa-md">
|
<div class="violet-card q-pa-md">
|
||||||
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
|
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
|
||||||
<q-btn
|
<q-btn
|
||||||
@ -33,11 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
title="Пользователи"
|
title="Пользователи"
|
||||||
:rows="users"
|
:rows="profiles"
|
||||||
:columns="userColumns"
|
:columns="profileColumns"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@row-click="onRowClick"
|
@row-click="onRowClick"
|
||||||
:loading="loadingUsers"
|
:loading="loadingProfiles"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
/>
|
/>
|
||||||
@ -125,7 +125,7 @@
|
|||||||
<template v-if="dialogType === 'teams'">
|
<template v-if="dialogType === 'teams'">
|
||||||
Редактирование команды {{ dialogData.title || '' }}
|
Редактирование команды {{ dialogData.title || '' }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="dialogType === 'users'">
|
<template v-else-if="dialogType === 'profiles'">
|
||||||
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
|
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="dialogType === 'projects'">
|
<template v-else-if="dialogType === 'projects'">
|
||||||
@ -151,19 +151,23 @@
|
|||||||
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Users -->
|
<!-- Profiles -->
|
||||||
<template v-else-if="dialogType === 'users'">
|
<template v-else-if="dialogType === 'profiles'">
|
||||||
<q-input v-model="dialogData.login" label="Логин" dense autofocus clearable />
|
<q-input v-model="dialogData.first_name" label="Имя" dense autofocus clearable />
|
||||||
<q-input v-model="dialogData.name" label="Имя" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.last_name" label="Фамилия" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.email" label="Email" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.patronymic" label="Отчество" dense clearable class="q-mt-sm" />
|
||||||
<!-- Добавь другие поля, которые нужны для пользователя -->
|
<q-input v-model="dialogData.birthday" label="День рождения" dense clearable class="q-mt-sm" type="date" />
|
||||||
|
<q-input v-model="dialogData.email" label="Почта" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="dialogData.phone" label="Телефон" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="dialogData.role_id" label="Роль" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="dialogData.team_id" label="Команда" dense clearable class="q-mt-sm" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Projects -->
|
<!-- Projects -->
|
||||||
<template v-else-if="dialogType === 'projects'">
|
<template v-else-if="dialogType === 'projects'">
|
||||||
<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.repo_url" label="URL репозитория" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.repository_url" label="URL репозитория" dense clearable class="q-mt-sm" />
|
||||||
<!-- Другие поля проекта -->
|
<!-- Другие поля проекта -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -190,7 +194,7 @@
|
|||||||
@click="deleteItem"
|
@click="deleteItem"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="dialogType === 'users'"
|
v-if="dialogType === 'profiles'"
|
||||||
flat
|
flat
|
||||||
label="Удалить"
|
label="Удалить"
|
||||||
color="negative"
|
color="negative"
|
||||||
@ -213,7 +217,7 @@
|
|||||||
<q-space />
|
<q-space />
|
||||||
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="['teams', 'users', 'projects', 'contests'].includes(dialogType)"
|
v-if="['teams', 'profiles', 'projects', 'contests'].includes(dialogType)"
|
||||||
flat
|
flat
|
||||||
label="Сохранить"
|
label="Сохранить"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -239,8 +243,32 @@ import updateProject from '@/api/projects/updateProject.js'
|
|||||||
import deleteProjectById from '@/api/projects/deleteProject.js'
|
import deleteProjectById from '@/api/projects/deleteProject.js'
|
||||||
import createProject from '@/api/projects/createProject.js'
|
import createProject from '@/api/projects/createProject.js'
|
||||||
|
|
||||||
|
import fetchProfiles from '@/api/profiles/getProfiles.js'
|
||||||
|
import updateProfile from '@/api/profiles/updateProfile.js'
|
||||||
|
import deleteProfileById from '@/api/profiles/deleteProfile.js'
|
||||||
|
import createProfile from '@/api/profiles/createProfile.js'
|
||||||
|
|
||||||
|
// import fetchContests from '@/api/contests/getContests.js'
|
||||||
|
// import updateContest from '@/api/contests/updateContest.js'
|
||||||
|
// import deleteContestById from '@/api/contests/deleteContest.js'
|
||||||
|
// import createContest from '@/api/contests/createContest.js'
|
||||||
|
|
||||||
// Текущая вкладка — 'teams' или 'projects'
|
// Текущая вкладка — 'teams' или 'projects'
|
||||||
const tab = ref('teams')
|
const tab = ref('profiles')
|
||||||
|
|
||||||
|
// --- Profiles ---
|
||||||
|
const profiles = ref([])
|
||||||
|
const loadingProfiles = ref(false)
|
||||||
|
const profileColumns = [
|
||||||
|
{ name: 'first_name', label: 'Имя', field: 'first_name', sortable: true },
|
||||||
|
{ name: 'last_name', label: 'Фамилия', field: 'last_name', sortable: true },
|
||||||
|
{ name: 'patronymic', label: 'Отчество', field: 'patronymic', sortable: true },
|
||||||
|
{ name: 'birthday', label: 'День рождения', field: 'birthday', sortable: true },
|
||||||
|
{ name: 'email', label: 'Почта', field: 'email', sortable: true },
|
||||||
|
{ name: 'phone', label: 'Телефон', field: 'phone', sortable: true },
|
||||||
|
{ name: 'role_id', label: 'Роль', field: 'role_id', sortable: true },
|
||||||
|
{ name: 'team_id', label: 'Команда', field: 'team_id', sortable: true },
|
||||||
|
]
|
||||||
|
|
||||||
// --- Teams ---
|
// --- Teams ---
|
||||||
const teams = ref([])
|
const teams = ref([])
|
||||||
@ -261,6 +289,17 @@ const projectColumns = [
|
|||||||
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// --- Contests ---
|
||||||
|
const contests = ref([])
|
||||||
|
const loadingContests = ref(false)
|
||||||
|
const contestColumns = [
|
||||||
|
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
||||||
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||||
|
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Общие состояния для диалогов
|
// Общие состояния для диалогов
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogData = ref({})
|
const dialogData = ref({})
|
||||||
@ -308,6 +347,15 @@ async function saveChanges() {
|
|||||||
const newProject = await createProject(dialogData.value)
|
const newProject = await createProject(dialogData.value)
|
||||||
projects.value.push(newProject)
|
projects.value.push(newProject)
|
||||||
}
|
}
|
||||||
|
} else if (dialogType.value === 'profiles') {
|
||||||
|
if (dialogData.value.id) {
|
||||||
|
await updateProfile(dialogData.value)
|
||||||
|
const idx = profiles.value.findIndex(p => p.id === dialogData.value.id)
|
||||||
|
if (idx !== -1) profiles.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
||||||
|
} else {
|
||||||
|
const newProfile = await createProfile(dialogData.value)
|
||||||
|
profiles.value.push(newProfile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -336,6 +384,16 @@ async function loadData(name) {
|
|||||||
} finally {
|
} finally {
|
||||||
loadingProjects.value = false
|
loadingProjects.value = false
|
||||||
}
|
}
|
||||||
|
} else if (name === 'profiles') {
|
||||||
|
loadingProfiles.value = true
|
||||||
|
try {
|
||||||
|
profiles.value = await fetchProfiles() || []
|
||||||
|
} catch (error) {
|
||||||
|
projects.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingProfiles.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,6 +406,9 @@ async function deleteItem() {
|
|||||||
} else if (dialogType.value === 'projects') {
|
} else if (dialogType.value === 'projects') {
|
||||||
await deleteProjectById(dialogData.value.id)
|
await deleteProjectById(dialogData.value.id)
|
||||||
projects.value = projects.value.filter(p => p.id !== dialogData.value.id)
|
projects.value = projects.value.filter(p => p.id !== dialogData.value.id)
|
||||||
|
} else if (dialogType.value === 'profiles') {
|
||||||
|
await deleteProfileById(dialogData.value.id)
|
||||||
|
profiles.value = profiles.value.filter(p => p.id !== dialogData.value.id)
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -186,7 +186,7 @@ const contests = ref([
|
|||||||
// --- Активность ---
|
// --- Активность ---
|
||||||
const activityData = ref([]);
|
const activityData = ref([]);
|
||||||
const dayHeight = 14;
|
const dayHeight = 14;
|
||||||
const squareSize = ref(14);
|
const squareSize = ref(12);
|
||||||
|
|
||||||
// Подписи месяцев (с июня 2024 по май 2025, чтобы соответствовать текущему году)
|
// Подписи месяцев (с июня 2024 по май 2025, чтобы соответствовать текущему году)
|
||||||
const monthLabels = ['июн.', 'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.', 'янв.', 'февр.', 'март', 'апр.', 'май'];
|
const monthLabels = ['июн.', 'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.', 'янв.', 'февр.', 'март', 'апр.', 'май'];
|
||||||
@ -241,7 +241,7 @@ function getMonthMargin(idx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка активности из API
|
// Загрузка активности из API
|
||||||
const username = 'andrei';
|
const username = 'Numerum';
|
||||||
|
|
||||||
async function loadActivity() {
|
async function loadActivity() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user