diff --git a/API/app/application/contest_statuses_repository.py b/API/app/application/contest_statuses_repository.py index 0857191..4b6bf6f 100644 --- a/API/app/application/contest_statuses_repository.py +++ b/API/app/application/contest_statuses_repository.py @@ -3,14 +3,14 @@ from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.domain.models import Contest +from app.domain.models import ContestStatus 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) + async def get_by_id(self, contest_status_id: int) -> Optional[ContestStatus]: + stmt = select(ContestStatus).filter_by(id=contest_status_id) result = await self.db.execute(stmt) return result.scalars().first() diff --git a/API/app/contollers/contests_router.py b/API/app/contollers/contests_router.py index febdaed..0876aec 100644 --- a/API/app/contollers/contests_router.py +++ b/API/app/contollers/contests_router.py @@ -69,20 +69,3 @@ async def delete_contest( 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/domain/entities/contest.py b/API/app/domain/entities/contest.py index dd2b31b..196e79a 100644 --- a/API/app/domain/entities/contest.py +++ b/API/app/domain/entities/contest.py @@ -2,7 +2,7 @@ from typing import Optional from pydantic import BaseModel class ContestEntity(BaseModel): - id: int + id: Optional[int] = None title: str description: Optional[str] = None web_url: str diff --git a/API/app/infrastructure/contests_service.py b/API/app/infrastructure/contests_service.py index a70f118..fcec495 100644 --- a/API/app/infrastructure/contests_service.py +++ b/API/app/infrastructure/contests_service.py @@ -15,7 +15,7 @@ class ContestsService: def __init__(self, db: AsyncSession): self.contests_repository = ContestsRepository(db) self.projects_repository = ProjectsRepository(db) - self.statuses_repository = ContestStatusesRepository(db) + self.contest_statuses_repository = ContestStatusesRepository(db) self.users_repository = UsersRepository(db) async def get_all_contests(self) -> list[ContestEntity]: @@ -33,7 +33,7 @@ class ContestsService: detail='The project with this ID was not found', ) - status_contest = await self.statuses_repository.get_by_id(contest.status_id) + status_contest = await self.contest_statuses_repository.get_by_id(contest.status_id) if status_contest is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -75,7 +75,7 @@ class ContestsService: detail='The project with this ID was not found', ) - status_contest = await self.statuses_repository.get_by_id(contest.role_id) + status_contest = await self.contest_statuses_repository.get_by_id(contest.status_id) if status_contest is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -114,17 +114,6 @@ class ContestsService: 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( diff --git a/WEB/src/api/contests/createProfile.js b/WEB/src/api/contests/createContest.js similarity index 77% rename from WEB/src/api/contests/createProfile.js rename to WEB/src/api/contests/createContest.js index 580bcec..b2fcb9f 100644 --- a/WEB/src/api/contests/createProfile.js +++ b/WEB/src/api/contests/createContest.js @@ -1,13 +1,13 @@ import axios from 'axios' import CONFIG from '@/core/config.js' -const createProfile = async (profile) => { - console.log(profile) +const createContest = async (contest) => { + console.log(contest) try { const token = localStorage.getItem('access_token') // или другой способ получения токена const response = await axios.post( - `${CONFIG.BASE_URL}/profiles`, - profile, + `${CONFIG.BASE_URL}/contests/`, + contest, { withCredentials: true, headers: { @@ -21,4 +21,4 @@ const createProfile = async (profile) => { } } -export default createProfile +export default createContest diff --git a/WEB/src/api/contests/deleteProfile.js b/WEB/src/api/contests/deleteContest.js similarity index 80% rename from WEB/src/api/contests/deleteProfile.js rename to WEB/src/api/contests/deleteContest.js index 2438bbb..3f0bc47 100644 --- a/WEB/src/api/contests/deleteProfile.js +++ b/WEB/src/api/contests/deleteContest.js @@ -1,11 +1,11 @@ import axios from 'axios' import CONFIG from '@/core/config.js' -const deleteProfile = async (profileId) => { +const deleteContest = async (contestId) => { try { const token = localStorage.getItem('access_token') // получение токена const response = await axios.delete( - `${CONFIG.BASE_URL}/profiles/${profileId}`, + `${CONFIG.BASE_URL}/contests/${contestId}`, { withCredentials: true, headers: { @@ -19,4 +19,4 @@ const deleteProfile = async (profileId) => { } } -export default deleteProfile +export default deleteContest diff --git a/WEB/src/api/contests/getContests.js b/WEB/src/api/contests/getContests.js index 01ca95e..1fd4bda 100644 --- a/WEB/src/api/contests/getContests.js +++ b/WEB/src/api/contests/getContests.js @@ -3,7 +3,7 @@ import CONFIG from "@/core/config.js"; const fetchContests = async () => { try { - const response = await axios.get(`${CONFIG.BASE_URL}/contests`, { + const response = await axios.get(`${CONFIG.BASE_URL}/contests/`, { withCredentials: true, }); return response.data; diff --git a/WEB/src/api/contests/getProfiles.js b/WEB/src/api/contests/getProfiles.js deleted file mode 100644 index 97f0824..0000000 --- a/WEB/src/api/contests/getProfiles.js +++ /dev/null @@ -1,25 +0,0 @@ -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/updateContest.js similarity index 86% rename from WEB/src/api/contests/updateProfile.js rename to WEB/src/api/contests/updateContest.js index 83c5a17..57ef747 100644 --- a/WEB/src/api/contests/updateProfile.js +++ b/WEB/src/api/contests/updateContest.js @@ -1,7 +1,7 @@ import axios from 'axios' import CONFIG from '@/core/config.js' -const updateProfile = async (profile) => { +const updateContest = async (profile) => { try { const token = localStorage.getItem('access_token') @@ -11,7 +11,7 @@ const updateProfile = async (profile) => { console.log('Отправляем на сервер:', profileData) const response = await axios.put( - `${CONFIG.BASE_URL}/profiles/${id}`, + `${CONFIG.BASE_URL}/contests/${id}/`, profileData, { withCredentials: true, @@ -28,4 +28,4 @@ const updateProfile = async (profile) => { } } -export default updateProfile +export default updateContest diff --git a/WEB/src/api/profiles/createProfile.js b/WEB/src/api/profiles/createProfile.js index 580bcec..5b7c8c6 100644 --- a/WEB/src/api/profiles/createProfile.js +++ b/WEB/src/api/profiles/createProfile.js @@ -2,11 +2,10 @@ 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`, + `${CONFIG.BASE_URL}/profiles/`, profile, { withCredentials: true, diff --git a/WEB/src/api/profiles/deleteProfile.js b/WEB/src/api/profiles/deleteProfile.js index 2438bbb..820e013 100644 --- a/WEB/src/api/profiles/deleteProfile.js +++ b/WEB/src/api/profiles/deleteProfile.js @@ -5,7 +5,7 @@ const deleteProfile = async (profileId) => { try { const token = localStorage.getItem('access_token') // получение токена const response = await axios.delete( - `${CONFIG.BASE_URL}/profiles/${profileId}`, + `${CONFIG.BASE_URL}/profiles/${profileId}/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/profiles/getProfiles.js b/WEB/src/api/profiles/getProfiles.js index 97f0824..2138986 100644 --- a/WEB/src/api/profiles/getProfiles.js +++ b/WEB/src/api/profiles/getProfiles.js @@ -5,7 +5,7 @@ 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`, { + const response = await axios.get(`${CONFIG.BASE_URL}/profiles/`, { headers: { Authorization: `Bearer ${token}`, }, diff --git a/WEB/src/api/profiles/updateProfile.js b/WEB/src/api/profiles/updateProfile.js index 83c5a17..6b2b114 100644 --- a/WEB/src/api/profiles/updateProfile.js +++ b/WEB/src/api/profiles/updateProfile.js @@ -8,10 +8,8 @@ const updateProfile = async (profile) => { // Убираем id из тела запроса, он идет в URL const { id, ...profileData } = profile - console.log('Отправляем на сервер:', profileData) - const response = await axios.put( - `${CONFIG.BASE_URL}/profiles/${id}`, + `${CONFIG.BASE_URL}/profiles/${id}/`, profileData, { withCredentials: true, @@ -21,7 +19,6 @@ const updateProfile = async (profile) => { } ) - console.log('Ответ от сервера:', response.data) return response.data } catch (error) { throw new Error(error.response?.data?.detail || error.message) diff --git a/WEB/src/api/projects/createProject.js b/WEB/src/api/projects/createProject.js index 88592cd..8f19b8e 100644 --- a/WEB/src/api/projects/createProject.js +++ b/WEB/src/api/projects/createProject.js @@ -2,11 +2,10 @@ 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( - `${CONFIG.BASE_URL}/projects`, + `${CONFIG.BASE_URL}/projects/`, project, { withCredentials: true, diff --git a/WEB/src/api/projects/deleteProject.js b/WEB/src/api/projects/deleteProject.js index 0f5f03e..0027e68 100644 --- a/WEB/src/api/projects/deleteProject.js +++ b/WEB/src/api/projects/deleteProject.js @@ -5,7 +5,7 @@ const deleteProject = async (projectId) => { try { const token = localStorage.getItem('access_token') // получение токена const response = await axios.delete( - `${CONFIG.BASE_URL}/projects/${projectId}`, + `${CONFIG.BASE_URL}/projects/${projectId}/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/projects/getProjects.js b/WEB/src/api/projects/getProjects.js index c3daf6d..bb4f226 100644 --- a/WEB/src/api/projects/getProjects.js +++ b/WEB/src/api/projects/getProjects.js @@ -6,7 +6,7 @@ const fetchProjects = async () => { const token = localStorage.getItem("access_token"); const response = await axios.get(`${CONFIG.BASE_URL}/projects`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${token}/`, }, }); diff --git a/WEB/src/api/projects/updateProject.js b/WEB/src/api/projects/updateProject.js index 02b3bb3..1970a51 100644 --- a/WEB/src/api/projects/updateProject.js +++ b/WEB/src/api/projects/updateProject.js @@ -10,7 +10,7 @@ const updateProject = async (project) => { const response = await axios.put( - `${CONFIG.BASE_URL}/projects/${id}`, + `${CONFIG.BASE_URL}/projects/${id}/`, projectData, { withCredentials: true, diff --git a/WEB/src/api/teams/createTeam.js b/WEB/src/api/teams/createTeam.js index 6f7c837..3f50d45 100644 --- a/WEB/src/api/teams/createTeam.js +++ b/WEB/src/api/teams/createTeam.js @@ -5,7 +5,7 @@ const createTeam = async (team) => { try { const token = localStorage.getItem('access_token') // или другой способ const response = await axios.post( - `${CONFIG.BASE_URL}/teams`, + `${CONFIG.BASE_URL}/teams/`, team, { withCredentials: true, diff --git a/WEB/src/api/teams/deleteTeam.js b/WEB/src/api/teams/deleteTeam.js index 9a53cb3..6c0faf2 100644 --- a/WEB/src/api/teams/deleteTeam.js +++ b/WEB/src/api/teams/deleteTeam.js @@ -5,7 +5,7 @@ const deleteTeam = async (teamId) => { try { const token = localStorage.getItem('access_token') // получение токена const response = await axios.delete( - `${CONFIG.BASE_URL}/teams/${teamId}`, + `${CONFIG.BASE_URL}/teams/${teamId}/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/teams/getTeams.js b/WEB/src/api/teams/getTeams.js index b6b6650..c50fff1 100644 --- a/WEB/src/api/teams/getTeams.js +++ b/WEB/src/api/teams/getTeams.js @@ -4,7 +4,7 @@ import CONFIG from "@/core/config.js"; const fetchTeams = async () => { try { const token = localStorage.getItem("access_token"); - const response = await axios.get(`${CONFIG.BASE_URL}/teams`, { + const response = await axios.get(`${CONFIG.BASE_URL}/teams/`, { headers: { Authorization: `Bearer ${token}`, }, diff --git a/WEB/src/api/teams/updateTeam.js b/WEB/src/api/teams/updateTeam.js index b73ddb3..6dfb380 100644 --- a/WEB/src/api/teams/updateTeam.js +++ b/WEB/src/api/teams/updateTeam.js @@ -10,7 +10,7 @@ const updateTeam = async (team) => { const response = await axios.put( - `${CONFIG.BASE_URL}/teams/${id}`, + `${CONFIG.BASE_URL}/teams/${id}/`, teamData, { withCredentials: true, diff --git a/WEB/src/main.js b/WEB/src/main.js index ccd84a5..af8923c 100644 --- a/WEB/src/main.js +++ b/WEB/src/main.js @@ -30,7 +30,15 @@ import { QTooltip, QBanner, QSlideTransition, - Ripple + Ripple, + QToggle, + QList, + QSpinnerDots, + QCarouselSlide, + QCarousel, + QItemSection, + QItemLabel, + QItem, } from 'quasar' @@ -48,7 +56,9 @@ app.use(Quasar, { QLayout, QPageContainer, QPage, QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable, QSeparator, QCardActions, QDialog, QIcon, QSpace, - QAvatar, QTooltip, QBanner, QSlideTransition + QAvatar, QTooltip, QBanner, QSlideTransition, QToggle, + QList, QSpinnerDots, QCarouselSlide, QCarousel, + QItemSection, QItemLabel, QItem }, directives: { Ripple diff --git a/WEB/src/pages/AdminPage.vue b/WEB/src/pages/AdminPage.vue index 3450a61..95cc77a 100644 --- a/WEB/src/pages/AdminPage.vue +++ b/WEB/src/pages/AdminPage.vue @@ -175,9 +175,12 @@ @@ -248,10 +261,12 @@ 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' +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' +import {Notify} from "quasar"; +import router from "@/router/index.js"; // Текущая вкладка — 'teams' или 'projects' const tab = ref('profiles') @@ -295,11 +310,14 @@ 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 }, + { name: 'web_url', label: 'Репозиторий', field: 'repository_url', sortable: true }, + { name: 'photo', label: 'Фото', field: 'photo', sortable: true }, + { name: 'results', label: 'Результаты', field: 'results', sortable: true }, + { name: 'is_win', label: 'Победа', field: 'is_win', sortable: true }, + { name: 'project_id', label: 'Проект', field: 'project_id', sortable: true }, + { name: 'status_id', label: 'Статус', field: 'status_id', sortable: true }, ] - - // Общие состояния для диалогов const dialogVisible = ref(false) const dialogData = ref({}) @@ -356,6 +374,15 @@ async function saveChanges() { const newProfile = await createProfile(dialogData.value) profiles.value.push(newProfile) } + } else if (dialogType.value === 'contests') { + if (dialogData.value.id) { + await updateContest(dialogData.value) + const idx = contests.value.findIndex(c => c.id === dialogData.value.id) + if (idx !== -1) contests.value[idx] = JSON.parse(JSON.stringify(dialogData.value)) + } else { + const newContest = await createContest(dialogData.value) + profiles.value.push(newContest) + } } closeDialog() } catch (error) { @@ -394,6 +421,16 @@ async function loadData(name) { } finally { loadingProfiles.value = false } + } else if (name === 'contests') { + loadingContests.value = true + try { + contests.value = await fetchContests() || [] + } catch (error) { + contests.value = [] + console.error(error.message) + } finally { + loadingContests.value = false + } } } @@ -409,6 +446,9 @@ async function deleteItem() { } else if (dialogType.value === 'profiles') { await deleteProfileById(dialogData.value.id) profiles.value = profiles.value.filter(p => p.id !== dialogData.value.id) + } else if (dialogType.value === 'contests') { + await deleteContestById(dialogData.value.id) + contests.value = contests.value.filter(c => c.id !== dialogData.value.id) } closeDialog() } catch (error) { @@ -417,6 +457,7 @@ async function deleteItem() { } function createHandler() { + dialogData.value = {}; openEdit(tab.value, null) } @@ -435,6 +476,24 @@ onMounted(() => { watch(tab, (newTab) => { loadData(newTab) }) + +const handleAuthAction = () => { + const isAuthenticated = ref(!!localStorage.getItem('access_token')) + if (isAuthenticated.value) { + localStorage.removeItem('access_token') + localStorage.removeItem('user_id') + isAuthenticated.value = false + + + Notify.create({ + type: 'positive', + message: 'Выход успешно осуществлен', + icon: 'check_circle', + }) + router.push('/') + } +} + \ No newline at end of file diff --git a/WEB/src/pages/HomePage.vue b/WEB/src/pages/HomePage.vue index a843397..e341159 100644 --- a/WEB/src/pages/HomePage.vue +++ b/WEB/src/pages/HomePage.vue @@ -46,9 +46,8 @@ :key="contest.id" class="contest-card violet-card" bordered - style="width: 220px;" - v-ripple - > + style="width: 220px; cursor: pointer;" v-ripple + @click="router.push({ name: 'contest-detail', params: { id: contest.id } })" >
{{ contest.title }}
{{ contest.description }}
@@ -241,7 +240,7 @@ function getMonthMargin(idx) { } // Загрузка активности из API -const username = 'Numerum'; +const username = 'archibald'; async function loadActivity() { try { diff --git a/WEB/src/router/index.js b/WEB/src/router/index.js index e396b8e..361dec1 100644 --- a/WEB/src/router/index.js +++ b/WEB/src/router/index.js @@ -2,11 +2,17 @@ import { createRouter, createWebHistory } from 'vue-router' import LoginPage from "../pages/LoginPage.vue" import HomePage from "../pages/HomePage.vue" import AdminPage from "../pages/AdminPage.vue" +import ContestDetailPage from "@/pages/ContestDetailPage.vue"; const routes = [ { path: '/', component: HomePage }, { path: '/login', component: LoginPage }, - { path: '/admin', component: AdminPage } + { path: '/admin', component: AdminPage }, + { + path: '/contests/:id', // Динамический маршрут, :id будет ID конкурса + name: 'contest-detail', // Имя маршрута для удобства навигации (например, router.push({ name: 'contest-detail', params: { id: 123 } })) + component: ContestDetailPage // Компонент, который будет отображаться + } ] const router = createRouter({