diff --git a/API/app/application/contest_statuses_repository.py b/API/app/application/contest_statuses_repository.py new file mode 100644 index 0000000..0857191 --- /dev/null +++ b/API/app/application/contest_statuses_repository.py @@ -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() diff --git a/API/app/application/contests_repository.py b/API/app/application/contests_repository.py new file mode 100644 index 0000000..0b4cb6c --- /dev/null +++ b/API/app/application/contests_repository.py @@ -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 diff --git a/API/app/application/profiles_repository.py b/API/app/application/profiles_repository.py index 29af714..b4f1b2b 100644 --- a/API/app/application/profiles_repository.py +++ b/API/app/application/profiles_repository.py @@ -1,6 +1,6 @@ from typing import Optional -from sqlalchemy import select +from sqlalchemy import select, Sequence from sqlalchemy.ext.asyncio import AsyncSession from app.domain.models import Profile @@ -10,6 +10,11 @@ class ProfilesRepository: def __init__(self, db: AsyncSession): 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]: stmt = select(Profile).filter_by(id=profile_id) result = await self.db.execute(stmt) diff --git a/API/app/contollers/contests_router.py b/API/app/contollers/contests_router.py new file mode 100644 index 0000000..febdaed --- /dev/null +++ b/API/app/contollers/contests_router.py @@ -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 + + diff --git a/API/app/contollers/profiles_router.py b/API/app/contollers/profiles_router.py index 0c4f4b4..0335774 100644 --- a/API/app/contollers/profiles_router.py +++ b/API/app/contollers/profiles_router.py @@ -1,6 +1,6 @@ from typing import Optional -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_db @@ -10,6 +10,18 @@ from app.infrastructure.profiles_service import ProfilesService 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( '/', diff --git a/API/app/domain/entities/contest.py b/API/app/domain/entities/contest.py new file mode 100644 index 0000000..dd2b31b --- /dev/null +++ b/API/app/domain/entities/contest.py @@ -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 \ No newline at end of file diff --git a/API/app/infrastructure/contests_service.py b/API/app/infrastructure/contests_service.py new file mode 100644 index 0000000..a70f118 --- /dev/null +++ b/API/app/infrastructure/contests_service.py @@ -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 diff --git a/API/app/infrastructure/profiles_service.py b/API/app/infrastructure/profiles_service.py index 4b4336b..f7b7c04 100644 --- a/API/app/infrastructure/profiles_service.py +++ b/API/app/infrastructure/profiles_service.py @@ -18,6 +18,13 @@ class ProfilesService: self.roles_repository = RolesRepository(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]: team = await self.teams_repository.get_by_id(profile.team_id) if team is None: diff --git a/API/app/main.py b/API/app/main.py index 4235d4d..f60e89b 100644 --- a/API/app/main.py +++ b/API/app/main.py @@ -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.teams_router import router as team_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 @@ -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(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(contest_router,prefix=f'{settings.PREFIX}/contests', tags=['contests']) return api_app diff --git a/WEB/src/api/contests/createProfile.js b/WEB/src/api/contests/createProfile.js new file mode 100644 index 0000000..580bcec --- /dev/null +++ b/WEB/src/api/contests/createProfile.js @@ -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 diff --git a/WEB/src/api/contests/deleteProfile.js b/WEB/src/api/contests/deleteProfile.js new file mode 100644 index 0000000..2438bbb --- /dev/null +++ b/WEB/src/api/contests/deleteProfile.js @@ -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 diff --git a/WEB/src/api/contests/getProfiles.js b/WEB/src/api/contests/getProfiles.js new file mode 100644 index 0000000..97f0824 --- /dev/null +++ b/WEB/src/api/contests/getProfiles.js @@ -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; diff --git a/WEB/src/api/contests/updateProfile.js b/WEB/src/api/contests/updateProfile.js new file mode 100644 index 0000000..83c5a17 --- /dev/null +++ b/WEB/src/api/contests/updateProfile.js @@ -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 diff --git a/WEB/src/api/profiles/createProfile.js b/WEB/src/api/profiles/createProfile.js new file mode 100644 index 0000000..580bcec --- /dev/null +++ b/WEB/src/api/profiles/createProfile.js @@ -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 diff --git a/WEB/src/api/profiles/deleteProfile.js b/WEB/src/api/profiles/deleteProfile.js new file mode 100644 index 0000000..2438bbb --- /dev/null +++ b/WEB/src/api/profiles/deleteProfile.js @@ -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 diff --git a/WEB/src/api/profiles/getProfiles.js b/WEB/src/api/profiles/getProfiles.js new file mode 100644 index 0000000..97f0824 --- /dev/null +++ b/WEB/src/api/profiles/getProfiles.js @@ -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; diff --git a/WEB/src/api/profiles/getUserProfile.js b/WEB/src/api/profiles/getUserProfile.js deleted file mode 100644 index 9070314..0000000 --- a/WEB/src/api/profiles/getUserProfile.js +++ /dev/null @@ -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 diff --git a/WEB/src/api/profiles/updateProfile.js b/WEB/src/api/profiles/updateProfile.js new file mode 100644 index 0000000..83c5a17 --- /dev/null +++ b/WEB/src/api/profiles/updateProfile.js @@ -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 diff --git a/WEB/src/api/projects/createProject.js b/WEB/src/api/projects/createProject.js index abebdd5..88592cd 100644 --- a/WEB/src/api/projects/createProject.js +++ b/WEB/src/api/projects/createProject.js @@ -2,6 +2,7 @@ import axios from 'axios' import CONFIG from '@/core/config.js' const createProject = async (project) => { + console.log(project) try { const token = localStorage.getItem('access_token') // или другой способ получения токена const response = await axios.post( diff --git a/WEB/src/api/projects/updateProject.js b/WEB/src/api/projects/updateProject.js index dee0132..02b3bb3 100644 --- a/WEB/src/api/projects/updateProject.js +++ b/WEB/src/api/projects/updateProject.js @@ -8,7 +8,6 @@ const updateProject = async (project) => { // Убираем id из тела запроса, он идет в URL const { id, ...projectData } = project - console.log('Отправляем на сервер:', projectData) const response = await axios.put( `${CONFIG.BASE_URL}/projects/${id}`, @@ -21,7 +20,6 @@ const updateProject = async (project) => { } ) - console.log('Ответ от сервера:', response.data) return response.data } catch (error) { throw new Error(error.response?.data?.detail || error.message) diff --git a/WEB/src/api/teams/updateTeam.js b/WEB/src/api/teams/updateTeam.js index 7a07193..b73ddb3 100644 --- a/WEB/src/api/teams/updateTeam.js +++ b/WEB/src/api/teams/updateTeam.js @@ -8,7 +8,6 @@ const updateTeam = async (team) => { // Убираем id из тела запроса, он идет в URL const { id, ...teamData } = team - console.log('Отправляем на сервер:', teamData) const response = await axios.put( `${CONFIG.BASE_URL}/teams/${id}`, @@ -21,7 +20,6 @@ const updateTeam = async (team) => { } ) - console.log('Ответ от сервера:', response.data) return response.data } catch (error) { throw new Error(error.response?.data?.detail || error.message) diff --git a/WEB/src/pages/AdminPage.vue b/WEB/src/pages/AdminPage.vue index de61057..3450a61 100644 --- a/WEB/src/pages/AdminPage.vue +++ b/WEB/src/pages/AdminPage.vue @@ -11,7 +11,7 @@ animated @update:model-value="loadData" > - + @@ -22,7 +22,7 @@ - +
@@ -125,7 +125,7 @@ -