diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 5fabaac..0e5fae9 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -2,4 +2,6 @@ sidebar_position: 1 --- -# Начала! \ No newline at end of file +# Начало! + +Выбирай интересующий раздел слева и просвещайся! diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 6665170..03ed198 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -1,77 +1,81 @@ -import {themes as prismThemes} from 'prism-react-renderer'; -import type {Config} from '@docusaurus/types'; -import type * as Preset from '@docusaurus/preset-classic'; +import { themes as prismThemes } from "prism-react-renderer"; +import type { Config } from "@docusaurus/types"; +import type * as Preset from "@docusaurus/preset-classic"; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) const config: Config = { - title: 'DataRush', - tagline: 'Изучите основы анализа данных здесь!', - favicon: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', + title: "DataRush", + tagline: "Изучите основы анализа данных здесь!", + favicon: "https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg", - url: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru', - baseUrl: '/docs/', + url: "https://prod-team-15-2pc0i3lc.final.prodcontest.ru", + baseUrl: "/docs/", - organizationName: 'megazord', - projectName: 'megazord', + organizationName: "megazord", + projectName: "megazord", - onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", - i18n: { - defaultLocale: 'ru', - locales: ['ru'], - }, - - presets: [ - [ - 'classic', - { - docs: {}, - theme: { - customCss: './src/css/custom.css', - }, - } satisfies Preset.Options, - ], - ], - - themeConfig: { - image: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', - navbar: { - title: 'DataRush', - logo: { - alt: 'DataRush', - src: 'https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg', - }, - items: [ - { - type: 'docSidebar', - sidebarId: 'defaultSidebar', - position: 'left', - label: 'Документация', - }, - ], + i18n: { + defaultLocale: "ru", + locales: ["ru"], }, - footer: { - style: 'dark', - links: [ - { - title: 'Документация', - items: [ + + presets: [ + [ + "classic", { - label: 'Начало', - to: '/docs/intro', + docs: { + routeBasePath: "/", + }, + theme: { + customCss: "./src/css/custom.css", + }, + } satisfies Preset.Options, + ], + ], + + themeConfig: { + image: "https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg", + navbar: { + title: "DataRush", + logo: { + alt: "DataRush", + src: "https://prod-team-15-2pc0i3lc.final.prodcontest.ru/dr.svg", }, - ], + items: [ + { + type: "docSidebar", + sidebarId: "defaultSidebar", + position: "left", + label: "Документация", + }, + ], }, - ], - copyright: `Создано для Megazord ♥`, - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - }, - } satisfies Preset.ThemeConfig, + footer: { + style: "dark", + links: [ + { + title: "Документация", + items: [ + { + label: "Начало", + to: "/intro", + }, + ], + }, + ], + copyright: `Создано для Megazord ♥`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, + + staticDirectories: ["static"], }; export default config; diff --git a/services/backend/api/v1/auth.py b/services/backend/api/v1/auth.py index 1d36ce9..4284e49 100644 --- a/services/backend/api/v1/auth.py +++ b/services/backend/api/v1/auth.py @@ -19,7 +19,10 @@ class BearerAuth(HttpBearer): except Exception: raise AuthenticationError - user = User.objects.get(id=data["id"]) + try: + user = User.objects.get(id=data["id"]) + except User.DoesNotExist: + raise AuthenticationError return user @staticmethod diff --git a/services/backend/apps/competition/tests.py b/services/backend/apps/competition/tests.py index 8c33351..db60d92 100644 --- a/services/backend/apps/competition/tests.py +++ b/services/backend/apps/competition/tests.py @@ -1,5 +1,8 @@ import uuid +from datetime import timedelta, datetime, tzinfo +from dateutil.parser import isoparse +import pytz from django.contrib.auth.hashers import make_password from django.test import TestCase @@ -101,3 +104,158 @@ class CompetitionEndpointTests(TestCase): HTTP_AUTHORIZATION=header, ) self.assertEqual(response.status_code, expected_status) + + +class CompetitionsEndpointTests(TestCase): + def setUp(self): + self.user = User.objects.create( + email="user@example.com", + password=make_password("password123"), + username="t1wk4" + ) + + resp = self.client.post( + "/api/v1/sign-in", + data={"email": self.user.email, "password": "password123"}, + content_type="application/json", + ).json() + token = resp["token"] + + # Create test competitions + now = datetime.now(tz=pytz.utc) + self.competitions = [] + for i in range(1, 6): + competition = Competition.objects.create( + title=f"Competition {i}", + description=f"Description {i}", + type=Competition.CompetitionType.SOLO, + participation_type=( + Competition.CompetitionParticipationType.EDU if i % 2 == 0 + else Competition.CompetitionParticipationType.COMPETITIVE + ), + start_date=(now + timedelta(days=i)).isoformat(), + end_date=(now + timedelta(days=i + 7)).isoformat(), + ) + if i <= 2: + competition.participants.add(self.user) + self.competitions.append(competition) + + self.valid_headers = { + "HTTP_AUTHORIZATION": f"Bearer {token}" + } + + def get_url(self, params=None): + base_url = "/api/v1/competitions" + return f"{base_url}?{params}" if params else base_url + + def test_get_participating_competitions(self): + """Test filtering competitions where user is participating""" + response = self.client.get( + self.get_url("is_participating=true"), + **self.valid_headers + ) + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(len(data), 2) + self.assertEqual( + {item["id"] for item in data}, + {str(self.competitions[0].id), str(self.competitions[1].id)} + ) + + def test_competition_type_values(self): + """Test competition type choices are respected""" + response = self.client.get( + self.get_url("is_participating=true"), + **self.valid_headers + ) + + for item in response.json(): + self.assertEqual(item["type"], "solo") + + def test_participation_type_values(self): + """Test participation type alternates between edu/competitive""" + response = self.client.get( + self.get_url("is_participating=false"), + **self.valid_headers + ) + + types = [item["participation_type"] for item in response.json()] + self.assertCountEqual( + types, + ["competitive", "edu", "competitive"] + ) + + def test_datetime_formatting(self): + """Test start/end date ISO formatting""" + response = self.client.get( + self.get_url("is_participating=true"), + **self.valid_headers + ) + + for item in response.json(): + if item["start_date"]: + try: + isoparse(item["start_date"]) + except ValueError: + self.fail("Invalid start_date format") + if item["end_date"]: + try: + isoparse(item["end_date"]) + except ValueError: + self.fail("Invalid end_date format") + + def test_competition_metadata(self): + """Test competition metadata fields""" + response = self.client.get( + self.get_url("is_participating=true"), + **self.valid_headers + ) + + item = response.json()[0] + self.assertEqual(item["title"], "Competition 1") + self.assertEqual(item["description"], "Description 1") + self.assertEqual(item["type"], "solo") + self.assertEqual(item["participation_type"], "competitive") + + def test_verbose_name_consistency(self): + """Test model verbose names don't affect API schema""" + response = self.client.get( + self.get_url("is_participating=true"), + **self.valid_headers + ) + + item = response.json()[0] + self.assertNotIn("название", item) # Russian verbose name + self.assertIn("title", item) # Actual API field name + + def test_null_dates_handling(self): + """Test competitions with null dates""" + competition = Competition.objects.create( + title="No Dates Competition", + description="Test competition", + type=Competition.CompetitionType.SOLO, + participation_type=Competition.CompetitionParticipationType.EDU + ) + + response = self.client.get( + self.get_url("is_participating=false"), + **self.valid_headers + ) + + test_item = next( + item for item in response.json() + if item["id"] == str(competition.id) + ) + self.assertIsNone(test_item["start_date"]) + self.assertIsNone(test_item["end_date"]) + + def test_participation_status_filtering(self): + """Test filtering by participation_type""" + response = self.client.get( + self.get_url("is_participating=false"), + **self.valid_headers + ) + + data = response.json() + self.assertEqual(len(data), 3) diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 2b3f33c..83bcfc3 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -60,6 +60,7 @@ class CompetitionTask(BaseModel): return self.title class Meta: + verbose_name = "задание" verbose_name_plural = "задания" diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml index def2053..056017b 100644 --- a/services/backend/pyproject.toml +++ b/services/backend/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "psycopg2-binary>=2.9.10", "pydantic>=2.10.5", "pyjwt>=2.10.1", + "python-dateutil>=2.9.0.post0", "python-gettext>=5.0", "python-json-logger>=3.2.1", "pytz>=2024.2", diff --git a/services/frontend/src/components/ui/icons/datarush.tsx b/services/frontend/src/components/ui/icons/datarush.tsx index ecc1627..0cebbe9 100644 --- a/services/frontend/src/components/ui/icons/datarush.tsx +++ b/services/frontend/src/components/ui/icons/datarush.tsx @@ -1,10 +1,17 @@ -const DataRush = ({ size = 52 }: { size?: number }) => { +const DataRush = ({ + size = 52, + className, +}: { + size?: number; + className?: string; +}) => { return ( { }, []); return ( -
- +
+

Добро пожаловать! diff --git a/services/frontend/src/widgets/navbar-layout.tsx b/services/frontend/src/widgets/navbar-layout.tsx index a8cd738..24c1bfe 100644 --- a/services/frontend/src/widgets/navbar-layout.tsx +++ b/services/frontend/src/widgets/navbar-layout.tsx @@ -6,7 +6,7 @@ const NavbarLayout = () => { <>
-
+