diff --git a/Caddyfile b/Caddyfile index 5e4ff90..2a36f17 100644 --- a/Caddyfile +++ b/Caddyfile @@ -5,6 +5,7 @@ reverse_proxy /openapi.json localhost:8000 reverse_proxy /socket.io* localhost:8000 reverse_proxy /socket.io/* localhost:8000 + reverse_proxy /ms http://localhost:7700 } diff --git a/Pipfile b/Pipfile index dc9527d..fb202e1 100644 --- a/Pipfile +++ b/Pipfile @@ -22,6 +22,7 @@ jinja2 = "*" argon2-cffi = "*" sentry-sdk = "*" aiofiles = "*" +meilisearch = "*" [dev-packages] coverage = "*" @@ -32,3 +33,6 @@ black = "*" [requires] python_version = "3.10" + +[scripts] +format = "black ." \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 1018ce8..e11c0a0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "075e07880b28878645b61e807c6091ae0ecdef50b3486e047aa853023fc2ae33" + "sha256": "7e2c8532f63f702ec73822fc0a4cab8fdf54de46dcd6be01aee4fbd0e5a8937e" }, "pipfile-spec": 6, "requires": { @@ -299,7 +299,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "click": { @@ -544,7 +544,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "jinja2": { @@ -609,6 +609,14 @@ "markers": "python_version >= '3.7'", "version": "==2.1.1" }, + "meilisearch": { + "hashes": [ + "sha256:437037dec8730d047e790e94d9af43b59b076fa7511593739aae857eafd83a1b", + "sha256:907986a37b8072b3e27b19b4ee2bb7a1a6f4c9fae961f240aaceadec3adbbab2" + ], + "index": "pypi", + "version": "==0.18.2" + }, "multidict": { "hashes": [ "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", @@ -831,11 +839,11 @@ }, "pyparsing": { "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", + "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.8" }, "python-dotenv": { "hashes": [ @@ -928,6 +936,14 @@ "index": "pypi", "version": "==4.2.2" }, + "requests": { + "hashes": [ + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.27.1" + }, "rsa": { "hashes": [ "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", @@ -938,19 +954,19 @@ }, "sentry-sdk": { "hashes": [ - "sha256:32af1a57954576709242beb8c373b3dbde346ac6bd616921def29d68846fb8c3", - "sha256:38fd16a92b5ef94203db3ece10e03bdaa291481dd7e00e77a148aa0302267d47" + "sha256:02c0f79941964297341c14d53e4df09a81c949707bd91e2d5e66b565aad75c8b", + "sha256:afd9f92711556739172f3bc845a02fc921ca1560bee974ae65072ef6a30e3921" ], "index": "pypi", - "version": "==1.5.8" + "version": "==1.5.9" }, "setuptools": { "hashes": [ - "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a", - "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967" + "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", + "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" ], "markers": "python_version >= '3.7'", - "version": "==62.0.0" + "version": "==62.1.0" }, "six": { "hashes": [ @@ -965,7 +981,7 @@ "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==1.2.0" }, "sqlalchemy": { @@ -1454,11 +1470,11 @@ }, "pyparsing": { "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", + "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.8" }, "pytest": { "hashes": [ @@ -1482,7 +1498,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.7'", "version": "==2.0.1" } } diff --git a/classquiz/__init__.py b/classquiz/__init__.py index f91d596..8749237 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -5,7 +5,7 @@ from socketio import ASGIApp from classquiz.config import settings from classquiz.db import database -from classquiz.routers import users, quiz, utils, stats, storage +from classquiz.routers import users, quiz, utils, stats, storage, search from classquiz.socket_server import sio settings = settings() @@ -46,4 +46,5 @@ app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils") app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats") app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage") +app.include_router(search.router, tags=["search"], prefix="/api/v1/search") app.mount("/", ASGIApp(sio)) diff --git a/classquiz/config.py b/classquiz/config.py index 0cde720..e93f769 100644 --- a/classquiz/config.py +++ b/classquiz/config.py @@ -2,6 +2,7 @@ from functools import lru_cache import redis.asyncio as redis_lib from pydantic import BaseSettings, RedisDsn, PostgresDsn +import meilisearch as MeiliSearch from classquiz.storage import Storage @@ -25,6 +26,8 @@ class Settings(BaseSettings): access_token_expire_minutes: int = 30 cache_expiry: int = 86400 sentry_dsn: str | None = "https://4981de1f72f24fd7b5e21b8913b93a02@o661934.ingest.sentry.io/6254641" + meilisearch_url: str + meilisearch_index: str = "classquiz" # storage_backend storage_backend: str | None = "deta" @@ -52,3 +55,5 @@ storage: Storage = Storage( deta_id=settings().deta_project_id, storage_path=settings().storage_path, ) + +meilisearch = MeiliSearch.Client(settings().meilisearch_url) diff --git a/classquiz/db/models.py b/classquiz/db/models.py index a91c962..3da892b 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -1,5 +1,6 @@ import uuid from datetime import datetime +from typing import Optional import ormar from pydantic import BaseModel, Json @@ -32,7 +33,7 @@ class UserSession(ormar.Model): """ id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) - user: uuid.UUID = ormar.ForeignKey(User) + user: Optional[User] = ormar.ForeignKey(User) session_key: str = ormar.String(unique=True, max_length=64) created_at: datetime = ormar.DateTime(default=datetime.now()) ip_address: str = ormar.String(max_length=100, nullable=True) @@ -71,7 +72,7 @@ class Quiz(ormar.Model): description: str = ormar.String(max_length=300, nullable=True) created_at: datetime = ormar.DateTime(default=datetime.now()) updated_at: datetime = ormar.DateTime(default=datetime.now()) - user_id: uuid.UUID = ormar.UUID(foreign_key=User.id) + user_id: uuid.UUID = ormar.ForeignKey(User) questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) class Meta: diff --git a/classquiz/kahoot_importer/import_quiz.py b/classquiz/kahoot_importer/import_quiz.py index 8eb43d4..28eb5ec 100644 --- a/classquiz/kahoot_importer/import_quiz.py +++ b/classquiz/kahoot_importer/import_quiz.py @@ -5,7 +5,7 @@ from datetime import datetime from aiohttp import ClientSession -from classquiz.config import settings, storage +from classquiz.config import settings, storage, meilisearch from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User from classquiz.kahoot_importer.get import get as get_quiz @@ -88,7 +88,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: ) quiz_data = Quiz( id=quiz_id, - public=False, + public=True, title=quiz.kahoot.title, description=quiz.kahoot.description, created_at=datetime.now(), @@ -96,4 +96,14 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: user_id=user.id, questions=json.dumps(quiz_questions), ) + meilisearch.index(settings.meilisearch_index).add_documents( + [ + { + "id": str(quiz_data.id), + "title": quiz_data.title, + "description": quiz_data.description, + "user": (await User.objects.filter(id=quiz_data.user_id).first()).username, + } + ] + ) return await quiz_data.save() diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index 4ba7a51..6cd39df 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -8,14 +8,13 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from pydantic import ValidationError -from classquiz.auth import get_current_user, get_current_user_optional -from classquiz.config import redis, settings, storage +from classquiz.auth import get_current_user +from classquiz.config import redis, settings, storage, meilisearch from classquiz.db.models import Quiz, QuizInput, User, PlayGame from classquiz.kahoot_importer.import_quiz import import_quiz settings = settings() - router = APIRouter() @@ -31,11 +30,21 @@ async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_curren raise HTTPException(status_code=400, detail="image url is not valid") quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4()) await redis.delete("global_quiz_count") + meilisearch.index(settings.meilisearch_index).add_documents( + [ + { + "id": str(quiz.id), + "title": quiz.title, + "description": quiz.description, + "user": (await User.objects.filter(id=quiz.user_id).first()).username, + } + ] + ) return await quiz.save() @router.get("/get/{quiz_id}") -async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user_optional)): +async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user)): try: quiz_id = uuid.UUID(quiz_id) except ValueError: @@ -54,6 +63,19 @@ async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current return quiz +@router.get("/get/public/{quiz_id}", response_model=Quiz) +async def get_public_quiz(quiz_id: str): + try: + quiz_id = uuid.UUID(quiz_id) + except ValueError: + raise HTTPException(status_code=400, detail="badly formed quiz id") + quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True) + if quiz is None: + return JSONResponse(status_code=404, content={"detail": "quiz not found"}) + else: + return quiz + + @router.post("/start/{quiz_id}") async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): try: @@ -62,7 +84,9 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): raise HTTPException(status_code=400, detail="badly formed quiz id") quiz = await Quiz.objects.get_or_none(id=quiz_id, user_id=user.id) if quiz is None: - return JSONResponse(status_code=404, content={"detail": "quiz not found"}) + quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True) + if quiz is None: + return JSONResponse(status_code=404, content={"detail": "quiz not found"}) else: game_pin = randint(10000000, 99999999) game = PlayGame( @@ -112,11 +136,36 @@ async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends( else: # print(quiz_input) # print(quiz) + meilisearch.index(settings.meilisearch_index).update_documents( + [ + { + "id": str(quiz.id), + "title": quiz_input.title, + "description": quiz_input.description, + "user": (await User.objects.filter(id=quiz.user_id).first()).username, + } + ] + ) + if quiz.public and not quiz_input.public: + print("removing from meilisearch") + meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id)) + if not quiz.public and quiz_input.public: + meilisearch.index(settings.meilisearch_index).add_documents( + [ + { + "id": str(quiz.id), + "title": quiz_input.title, + "description": quiz_input.description, + "user": (await User.objects.filter(id=quiz.user_id).first()).username, + } + ] + ) quiz.title = quiz_input.title quiz.public = quiz_input.public quiz.description = quiz_input.description quiz.updated_at = datetime.now() quiz.questions = quiz_input.dict()["questions"] + return await quiz.update() @@ -149,4 +198,5 @@ async def delete_quiz(quiz_id: str, user: User = Depends(get_current_user)): pass if len(pics_to_delete) != 0: await storage.delete(pics_to_delete) + meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id)) return await quiz.delete() diff --git a/classquiz/routers/search.py b/classquiz/routers/search.py new file mode 100644 index 0000000..99c6c5a --- /dev/null +++ b/classquiz/routers/search.py @@ -0,0 +1,88 @@ +import pydantic +from fastapi import APIRouter + +from classquiz.config import settings, meilisearch +from uuid import UUID +from typing import Optional, List + +settings = settings() + +router = APIRouter() + + +class Hit(pydantic.BaseModel): + id: UUID + title: str + description: str + user: str + formatted: Optional["Hit"] = pydantic.Field(None, alias="_formatted") + + +class SearchResponse(pydantic.BaseModel): + hits: List[Hit] | list[None] + nbHits: int + exhaustiveNbHits: bool + query: str + limit: int + offset: int + processingTimeMs: int + + +class SearchData(pydantic.BaseModel): + q: str + offset: Optional[int] = 0 + limit: Optional[int] = 20 + filter: Optional[str] = None + facetsDistribution: Optional[list[str]] = None + attributesToRetrieve: Optional[list[str]] = ["*"] + attributesToCrop: Optional[list[str]] = None + cropLength: Optional[int] = 200 + attributesToHighlight: Optional[list[str]] = None + matches: Optional[bool] = False + sort: Optional[list[str]] = None + + +@router.post("/", response_model=SearchResponse) +async def search(data: SearchData): + index = meilisearch.get_index(settings.meilisearch_index) + query = index.search( + data.q, + { + "offset": data.offset, + "limit": data.limit, + "filter": data.filter, + "cropLength": data.cropLength, + "matches": data.matches, + "facetsDistribution": data.facetsDistribution, + "attributesToRetrieve": data.attributesToRetrieve, + "attributesToCrop": data.attributesToCrop, + "sort": data.sort, + "attributesToHighlight": data.attributesToHighlight, + }, + ) + return SearchResponse(**query) + + +@router.get("/", response_model=SearchResponse) +async def search_get( + q: str, + offset: int = 0, + limit: int = 20, + filter: str | None = None, # skipcq: PYL-W0622 + cropLength: int = 200, + matches: bool = False, + attributesToHighlight: Optional[str] = "*", +): + index = meilisearch.get_index(settings.meilisearch_index) + query = index.search( + q, + { + "offset": offset, + "limit": limit, + "filter": filter, # skipcq: PYL-W0622 + "cropLength": cropLength, + "matches": matches, + "attributesToHighlight": [attributesToHighlight], + }, + ) + return SearchResponse(**query) diff --git a/classquiz/scripts/__init__.py b/classquiz/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index b33fe8b..f4ef6d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,5 +59,13 @@ services: - ./Caddyfile-docker:/etc/caddy/Caddyfile ports: - "8000:8080" + meilisearch: + image: getmeili/meilisearch:latest + restart: always + environment: + MEILI_NO_ANALYTICS: true + volumes: + - meilisearch-data:/data.ms volumes: - data: \ No newline at end of file + data: + meilisearch-data: \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 3ae7142..d9856f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,9 +50,10 @@ "svelte": "^3.47.0", "svelte-check": "^2.6.0", "svelte-preprocess": "^4.10.5", - "sveltejs-tippy": "^3.0.0", + "svelte-tippy": "^1.3.2", "swiper": "^8.1.0", "tailwindcss": "^3.0.24", + "tippy.js": "^6.3.7", "tslib": "^2.3.1", "typescript": "~4.6", "yup": "^0.32.11" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9089db5..d65661f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -38,9 +38,10 @@ specifiers: svelte: ^3.47.0 svelte-check: ^2.6.0 svelte-preprocess: ^4.10.5 - sveltejs-tippy: ^3.0.0 + svelte-tippy: ^1.3.2 swiper: ^8.1.0 tailwindcss: ^3.0.24 + tippy.js: ^6.3.7 tslib: ^2.3.1 typescript: ~4.6 yup: ^0.32.11 @@ -55,7 +56,7 @@ devDependencies: '@sentry/tracing': 6.19.6 '@sveltejs/adapter-auto': 1.0.0-next.34 '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.311_sass@1.50.0+svelte@3.47.0 + '@sveltejs/kit': 1.0.0-next.312_sass@1.50.0+svelte@3.47.0 '@tailwindcss/typography': 0.5.2_tailwindcss@3.0.24 '@types/cookie': 0.4.1 '@types/js-cookie': 3.0.1 @@ -85,9 +86,10 @@ devDependencies: svelte: 3.47.0 svelte-check: 2.6.0_1b6d41eb44819e98e49be8cacdc96c52 svelte-preprocess: 4.10.5_f6bc70e70cf1a2fde5bc22fd7000ea23 - sveltejs-tippy: 3.0.0 + svelte-tippy: 1.3.2 swiper: 8.1.0 tailwindcss: 3.0.24 + tippy.js: 6.3.7 tslib: 2.3.1 typescript: 4.6.3 yup: 0.32.11 @@ -392,10 +394,10 @@ packages: esbuild: 0.14.36 dev: true - /@sveltejs/kit/1.0.0-next.311_sass@1.50.0+svelte@3.47.0: + /@sveltejs/kit/1.0.0-next.312_sass@1.50.0+svelte@3.47.0: resolution: { - integrity: sha512-OkRIi0XBXcs16J+H+6fgOeFYxKu05gamY08KHAQ5cohjymUODGWIwaIfwAuNkZAcEEr8Vh2ejp4UcaxNzD53lQ== + integrity: sha512-l07ct0FJQnt+y3/NSEBvHKDKVE/7syYQnFhCVZ6H+4LEYRtTNybFxUTC8jA3xpZwPqJLZz1X+Tt+Sa8ZMuVzDg== } engines: { node: '>=14.13' } hasBin: true @@ -3507,6 +3509,15 @@ packages: typescript: 4.6.3 dev: true + /svelte-tippy/1.3.2: + resolution: + { + integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw== + } + dependencies: + tippy.js: 6.3.7 + dev: true + /svelte/3.47.0: resolution: { @@ -3515,15 +3526,6 @@ packages: engines: { node: '>= 8' } dev: true - /sveltejs-tippy/3.0.0: - resolution: - { - integrity: sha512-LAfQikm61AeqZW1hCFWUsmGEMmhMHDvrPV0JhbT38Ell9SqBjmFaY//+YKUxaJvx6nLMxGQdlcHA1rDg2Ml8tQ== - } - dependencies: - tippy.js: 6.0.3 - dev: true - /svgo/2.8.0: resolution: { @@ -3600,10 +3602,10 @@ packages: globrex: 0.1.2 dev: true - /tippy.js/6.0.3: + /tippy.js/6.3.7: resolution: { - integrity: sha512-buQF6HugTA4YeSA/s9xBhu0ferEAx7sRSn45G+Juh3p+Dz3vEKfxtqdg6JUnHIWDCV/r0u8Lrobs9AB4dtYcUQ== + integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== } dependencies: '@popperjs/core': 2.11.5 diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index c44cce9..e69de29 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -1,44 +0,0 @@ -/// - -// See https://kit.svelte.dev/docs/types#the-app-namespace -// for information about these interfaces -declare namespace App { - // interface Locals {} - // interface Platform {} - interface Session { - authenticated: boolean; - token: string | null; - email: string | null; - } - - // interface Stuff {} -} - -export interface QuizData { - title: string; - description: string; - quiz_id: string; - questions: Question[]; - game_id: string; - game_pin: string; - started: boolean; -} - -export interface Question { - time: string; - question: string; - image?: string; - answers: Answer[]; -} - -export interface Answer { - right: boolean; - answer: string; -} - -// TODO Keep an eye on this shit -// export interface Answer { -// username: string; -// answer: string; -// right: boolean; -// } diff --git a/frontend/src/lib/collapsible.svelte b/frontend/src/lib/collapsible.svelte new file mode 100644 index 0000000..3ba3b19 --- /dev/null +++ b/frontend/src/lib/collapsible.svelte @@ -0,0 +1,59 @@ + + +
+

+ +

+ +
+ +
+
+ + diff --git a/frontend/src/lib/navbar.svelte b/frontend/src/lib/navbar.svelte index e28aafc..2709bc5 100644 --- a/frontend/src/lib/navbar.svelte +++ b/frontend/src/lib/navbar.svelte @@ -94,21 +94,22 @@
  • GitHubExplore
  • +
  • + Search + +
  • {$t('words.logout')}
  • -
  • - {$t('words.docs')} -
  • {:else}
  • {$t('words.register')}
  • +
  • {$t('words.login')}
  • +
  • + Explore +
  • +
  • + Search + +
  • - import type { QuizData } from '../../app'; + import type { QuizData } from '$lib/quiz_types'; export let quiz_data: QuizData; export let final_results: Array | Array>; diff --git a/frontend/src/lib/play/question.svelte b/frontend/src/lib/play/question.svelte index 1040bcc..3c6a93f 100644 --- a/frontend/src/lib/play/question.svelte +++ b/frontend/src/lib/play/question.svelte @@ -1,5 +1,5 @@ + +{#await getData()} + + + + +{:then quizzes} + +{/await} diff --git a/frontend/src/routes/overview.svelte b/frontend/src/routes/overview.svelte index e03d858..6656a60 100644 --- a/frontend/src/routes/overview.svelte +++ b/frontend/src/routes/overview.svelte @@ -15,7 +15,7 @@ + +
    +
    +
    +
    + + +
    +
    +
    +
    + +{#if resp_data} + +{/if} + + diff --git a/frontend/src/routes/view/[quiz_id].svelte b/frontend/src/routes/view/[quiz_id].svelte new file mode 100644 index 0000000..941d0ff --- /dev/null +++ b/frontend/src/routes/view/[quiz_id].svelte @@ -0,0 +1,159 @@ + + + + +
    +

    {quiz.title}

    +
    +

    {quiz.description}

    +
    + +
    + {#if logged_in} + + {:else} + + {/if} +
    + {#each quiz.questions as question, index_question} +
    + +
    +

    {$t('words.question')} {index_question + 1}

    + + +

    + {$t('words.question')}: {question.question} +

    + + {#if question.image} + + {$t('words.image')}: + Not provided + + {/if} + + {$t('editor.time_in_seconds')}: +

    {question.time}

    +
    + {#each question.answers as answer, index_answer} +
    +

    {$t('words.answer')} {index_answer + 1}

    +

    + {$t('words.answer')}: {index_answer + 1} + {$t('words.question')}: {index_question + 1} +

    +

    + {$t('words.answer')} + : {quiz.questions[index_question].answers[index_answer].answer} +

    + +
    + {/each} +
    +
    +
    + {/each} +
    diff --git a/import_to_meili.py b/import_to_meili.py new file mode 100644 index 0000000..dd655ad --- /dev/null +++ b/import_to_meili.py @@ -0,0 +1,27 @@ +import meilisearch +from classquiz.db.models import Quiz, User +from classquiz.config import settings +from asyncio import run + +settings = settings() + + +async def __main__(): + meili_data = [] + questions = await Quiz.objects.filter(public=True).all() + for question in questions: + meili_data.append( + { + "id": str(question.id), + "title": question.title, + "description": question.description, + "user": (await User.objects.filter(id=question.user_id).first()).username, + } + ) + print(meili_data) + client = meilisearch.Client(settings.meilisearch_url) + client.index(settings.meilisearch_index).add_documents(meili_data) + + +if __name__ == "__main__": + run(__main__())