From 5c5386fdf8e975169aad3afd6eb3852818e35fa4 Mon Sep 17 00:00:00 2001 From: Mawoka Date: Tue, 8 Mar 2022 21:20:53 +0100 Subject: [PATCH] :sparkles: Added get and search kahoot-thing --- classquiz/cache.py | 8 +- classquiz/kahoot_importer/__init__.py | 179 +++++++++++++++++++++++ classquiz/kahoot_importer/get.py | 111 +------------- classquiz/kahoot_importer/search.py | 34 +++++ classquiz/kahoot_importer/test_get.py | 66 ++++++++- classquiz/kahoot_importer/test_search.py | 20 +++ classquiz/routers/quiz.py | 2 +- classquiz/socket_server/__init__.py | 19 +-- 8 files changed, 313 insertions(+), 126 deletions(-) create mode 100644 classquiz/kahoot_importer/test_search.py diff --git a/classquiz/cache.py b/classquiz/cache.py index 8cef3f9..7ad4092 100644 --- a/classquiz/cache.py +++ b/classquiz/cache.py @@ -14,14 +14,14 @@ async def cache_account(criteria: str, content: str) -> Union[User, None]: if criteria == "email": try: - res = await User.objects.get(email=content, verified=True) + res = await User.objects.search() except ormar.exceptions.NoMatch: return None await insert_into_redis(res, content) return res elif criteria == "username": try: - res = await User.objects.get(username=content, verified=True) + res = await User.objects.search() except ormar.exceptions.NoMatch: return None await insert_into_redis(res, content) @@ -29,7 +29,7 @@ async def cache_account(criteria: str, content: str) -> Union[User, None]: elif criteria == "id": try: - res = await User.objects.get(id=uuid.UUID(content), verified=True) + res = await User.objects.search() except ormar.exceptions.NoMatch: return None await insert_into_redis(res, content) @@ -39,7 +39,7 @@ async def cache_account(criteria: str, content: str) -> Union[User, None]: async def get_from_redis(key: str) -> Union[None, User]: - user = await redis.get(key) + user = await redis.search(key) if user is None: return None else: diff --git a/classquiz/kahoot_importer/__init__.py b/classquiz/kahoot_importer/__init__.py index e69de29..cf7dd72 100644 --- a/classquiz/kahoot_importer/__init__.py +++ b/classquiz/kahoot_importer/__init__.py @@ -0,0 +1,179 @@ +from pydantic import BaseModel +from uuid import UUID +from typing import List, Any, Optional +from enum import Enum + + +class _CoverMetadata(BaseModel): + id: UUID + resources: str | None + + +class _CreatorAvatar(BaseModel): + url: str + id: UUID | None + type: str | None + bitmojiAvatarId: str | None + altText: str | None + contentType: str | None + width: int | None + height: int | None + + +class _LastEdit(BaseModel): + editorUserId: UUID + editorUsername: str + editTimestamp: int + + +class _ImageMetadata(BaseModel): + id: UUID | None + content_type: Optional[str] + width: Optional[int] + height: Optional[int] + resources: Optional[str] + + +class _SampleQuestion(BaseModel): + image: str | None + imageMetadata: _ImageMetadata | None + title: str + type: str + time: int | None + + +class _Access(BaseModel): + groupRead: List[Any | None] + folderGroupIds: List[Any | None] + + +class _Card(BaseModel): + type: str + title: str + description: str + slug: str + cover: str | None + coverMetadata: _CoverMetadata | dict[None, None] | None + draftExists: bool + inventoryItemIds: List[Any] + number_of_questions: int + creator: UUID + creator_username: str + creator_avatar: _CreatorAvatar | dict[None, None] | None + badges: List[str] + visibility: int + locked: bool + writeProtection: bool + lastEdit: _LastEdit | None + featured: bool + young_featured: bool + sponsored: bool + draft: bool + combined: bool + compatibility_level: int + sample_questions: List[_SampleQuestion] + number_of_plays: int + number_of_players: int + total_favourites: int + question_types: List[str] + created: int + modified: int + access: _Access + duplication_disabled: bool + uuid: UUID + + +class _Entity(BaseModel): + card: _Card + + +class _Origin(BaseModel): + x: int + y: int + + +class _Crop(BaseModel): + origin: _Origin + target: _Origin + circular: bool + + +# class _Language(Enum): +# EN_US = "en-US" + + +class _LanguageInfo(BaseModel): + language: str + lastUpdatedOn: int + readAloudSupported: bool + + +class _Metadata(BaseModel): + access: _Access + duplicationProtection: bool | None + lastEdit: _LastEdit | None + + +class _Parent(BaseModel): + id: UUID + creator_username: str + + +class _Choice(BaseModel): + answer: str + correct: bool + languageInfo: _LanguageInfo | None + + +class _Video(BaseModel): + startTime: float + endTime: float + service: str + full_url: Optional[str] + id: Optional[str] + + +class _Question(BaseModel): + type: str + question: str + time: int + points: bool + pointsMultiplier: int + choices: List[_Choice] + image: str | None + imageMetadata: _ImageMetadata | None + resources: Optional[str] + video: _Video + questionFormat: int + languageInfo: _LanguageInfo | None + media: List[Any] + + +class _Kahoot(BaseModel): + uuid: UUID + language: str + creator: UUID + creator_username: str + compatibilityLevel: int + creator_primary_usage: str + folderId: UUID | None + visibility: int + difficulty: int | None + audience: str + audience: str + title: str + description: str + quizType: str + tags: str | None | List[str] + cover: str | None + coverMetadata: _CoverMetadata | dict[None, None] | None + questions: List[_Question] + metadata: _Metadata + parent: _Parent | None + resources: str | None + slug: str + languageInfo: _LanguageInfo | None + inventoryItemIds: List[Any] + type: str + created: int + modified: int diff --git a/classquiz/kahoot_importer/get.py b/classquiz/kahoot_importer/get.py index 6124a99..e64fced 100644 --- a/classquiz/kahoot_importer/get.py +++ b/classquiz/kahoot_importer/get.py @@ -1,116 +1,15 @@ -from uuid import UUID -from typing import List, Any, Optional from aiohttp import ClientSession from pydantic import BaseModel - - -class _Access(BaseModel): - groupRead: List[Any | None] - folderGroupIds: List[Any | None] - - -class _CoverMetadata(BaseModel): - id: UUID - resources: str | None - - -class _CreatorAvatar(BaseModel): - url: str - id: UUID | None - type: str | None - bitmojiAvatarId: str | None - altText: str | None - contentType: str | None - width: int | None - height: int | None - - -class _LastEdit(BaseModel): - editorUserId: UUID - editorUsername: str - editTimestamp: int - - -class _ImageMetadata(BaseModel): - id: UUID - content_type: Optional[str] - width: Optional[int] - height: Optional[int] - resources: Optional[str] - - -class _SampleQuestion(BaseModel): - image: str | None - imageMetadata: _ImageMetadata | None - title: str - type: str - time: int - - -class _Card(BaseModel): - type: str - title: str - description: str - slug: str - cover: str | None - coverMetadata: _CoverMetadata | dict[None, None] - draftExists: bool - inventoryItemIds: List[Any] - number_of_questions: int - creator: UUID - creator_username: str - creator_avatar: _CreatorAvatar | dict[None, None] | None - badges: List[str] - visibility: int - locked: bool - writeProtection: bool - last_edit: _LastEdit | None - featured: bool - young_featured: bool - sponsored: bool - draft: bool - combined: bool - compatibility_level: int - sample_questions: List[_SampleQuestion] - number_of_plays: int - number_of_players: int - total_favourites: int - question_types: List[str] - created: int - modified: int - access: _Access - duplication_disabled: bool - uuid: UUID - - -class _Entity(BaseModel): - card: _Card +from classquiz.kahoot_importer import _Card, _Kahoot class _Response(BaseModel): - entities: List[_Entity] - totalHits: int - cursor: int - pageTimestamp: int + card: _Card + kahoot: _Kahoot -async def get(query: str | None, limit: int | None = 9, cursor: int | None = 1, - search_cluster: int | None = 1, inventory_item_id: str | None = "ANY") -> _Response: - """ - - :param inventory_item_id: I dkon't know - :param search_cluster: Doesn't seeem to matter - :param cursor: The position in the result-list (page) - :param query: The search query - :param limit: Less or equals 100 - :return: - """ +async def get(game_id: str) -> _Response: async with ClientSession() as session: - async with session.get( - f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" - - ) as response: - print( - f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}") + async with session.get(f"https://create.kahoot.it/rest/kahoots/{game_id}/card/?includeKahoot=true") as response: return _Response(**await response.json()) diff --git a/classquiz/kahoot_importer/search.py b/classquiz/kahoot_importer/search.py index e69de29..7776167 100644 --- a/classquiz/kahoot_importer/search.py +++ b/classquiz/kahoot_importer/search.py @@ -0,0 +1,34 @@ +from uuid import UUID +from typing import List, Any, Optional +from aiohttp import ClientSession +from classquiz.kahoot_importer import _Entity + +from pydantic import BaseModel + + +class _Response(BaseModel): + entities: List[_Entity] + totalHits: int + cursor: int + pageTimestamp: int + + +async def search(query: str | None, limit: int | None = 9, cursor: int | None = 1, + search_cluster: int | None = 1, inventory_item_id: str | None = "ANY") -> _Response: + """ + + :param inventory_item_id: I dkon't know + :param search_cluster: Doesn't seeem to matter + :param cursor: The position in the result-list (page) + :param query: The search query + :param limit: Less or equals 100 + :return: + """ + async with ClientSession() as session: + async with session.get( + f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" + + ) as response: + # print( + # f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}") + return _Response(**await response.json()) diff --git a/classquiz/kahoot_importer/test_get.py b/classquiz/kahoot_importer/test_get.py index 14c4325..5899b02 100644 --- a/classquiz/kahoot_importer/test_get.py +++ b/classquiz/kahoot_importer/test_get.py @@ -1,11 +1,65 @@ from unittest import IsolatedAsyncioTestCase -from get import get +from classquiz.kahoot_importer.get import get class Test(IsolatedAsyncioTestCase): async def test_get(self): - res = await get(query="Python") - self.assertEqual(len(res.entities), 9) - await get(query="Test Quiz") - await get(query="Biologie") - await get(query="Chemie") \ No newline at end of file + await get('1f95eb0b-fcf4-4db2-879b-5418ef75116b') + await get("c63d4d39-767e-4754-8c16-66c3c2d9633c") + await get("c5d31de6-4598-4574-8e67-ca9a2aba49f8") + await get("3d545fc3-b830-44b6-94ac-c8e4a3c9fb88") + quiz_list = ['c63d4d39-767e-4754-8c16-66c3c2d9633c', 'a73689d2-1108-4524-88e1-10c54282346c', + 'e8e2d869-7c75-4ad6-8872-031bc8e37fa9', '387b4139-9e77-4b47-93b7-dbb6d75c54d1', + 'a1fff657-b99d-4547-93cf-4e9629994844', '0880d792-a77e-4cba-8a8b-359de40ffe76', + 'e2d12f01-a3dd-4d0e-a6c4-29f38046117f', 'e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7', + 'e655c086-558d-4602-99fd-5fc429ffe0ce', '1642f29d-b5cb-4213-82e6-7c1fae401aca', + 'fabdd572-da3e-4621-b912-41b9bc5a18fb', 'ded3cbbf-5af3-4761-a6da-b3b6ca040e1a', + '370b3e33-ed97-4d36-9362-7f76aa9d12b9', 'cfa32a28-1a01-47f1-9bca-c85640bceeb8', + '993f615d-6890-4dd3-ac08-f462de9ac5a3', '7f08a377-e371-425e-95f9-7196401ae7ff', + '19656fed-5891-48e7-ae7f-1358f623bfd0', '5e766d32-930a-4640-82a1-87c58612f579', + 'ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5', 'dd3c9175-addd-48df-8297-7598d4f12006', + 'b8de862c-fc4b-4df6-9fa5-81b91868fc3b', '3b082f4e-9c50-45ea-98c4-3845b14e400c', + '10179f67-a344-4fd0-80a1-d1e1b6ca7953', '8288c1dd-9675-411c-9175-4180004da3c3', + 'af838c22-dcbb-497d-8c7b-f6499835b83d', '152c5fdb-3b0a-4cdf-a0d9-1984de0643ad', + '846fd2bb-6b95-4333-b5d4-ef0a363e73a9', 'dd6b446f-817b-469b-9e6b-e1d34c466a7f', + '2720d636-8918-4c6a-86a9-928c489382f4', 'a6985641-df97-4e56-97fc-e0901e32b464', + '1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9', '677fa485-4231-4479-beb2-fe757053bf41', + '2b6379f8-450e-46c0-a854-5e368a03d5a6', 'f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1', + '51d2e839-a9d7-4f26-b1fd-fb0743c3a236', 'f2212243-4553-456d-87e7-7ddfd6ee7563', + '2d6982d3-7b46-4022-bbb6-cb5451c581ba', '1cccae22-5140-46b5-ba6a-aa17cec14dc3', + 'b01dd2c2-ffcf-4c02-a274-e12296dbe4e1', '2669d193-a81f-4752-a003-dd2433beef92', + '06203ee6-3615-4e26-bb08-4fab249698a4', '933c1426-c759-43a3-9770-c8e3948db998', + '02046384-fb05-4f1d-855a-f0e52a0761e3', 'b5266e9b-7372-4df0-94a9-ec8a6c0aabc6', + 'fb8e3a3d-505a-4157-bd9a-b73b00591f10', '42092675-9171-400f-ab4d-c898ec9f70bd', + '1d6307e2-98c2-42f2-b765-d02a68e0da80', '8f431ed0-31df-4435-bcba-aff46929fe82', + 'd573d76f-d715-4a54-bdde-7ac7b6235fb2', 'feb8985b-84fb-4d41-bc41-70c322f508fa', + 'f9f945cc-c166-4f7d-a95f-f0be154c43cf', '009b130a-1709-41a6-943d-85eee4889792', + 'a3f765f7-2329-47e0-83eb-0837b4b54b43', 'fea9e558-206f-444a-8d7f-a3f0301fda8d', + '9b419098-ccfc-467d-9de0-db18b60d6c28', 'e6c2174e-3a3f-4193-9782-1bf3b5cfae63', + 'd42c962e-1691-4fa4-8926-c33157423bd2', '8d27aa83-a103-4f05-8230-69e857bc3ffd', + '60cd19b2-6858-4704-b07b-5196e8e24325', '31624ea4-1e4f-4f90-ae4f-759a0eb93b61', + 'fcd038f4-6b05-485b-afe3-ce67e05b1695', '1207bbe5-2df5-4c86-8f54-1b2675a86859', + 'eea3b0b0-deae-41de-b010-d6d50855d016', 'ca8b0278-744c-4a3f-a2b8-6ab29d76fa7f', + '68d37bc8-28b9-49fd-9e02-48a3c9195fdf', '8bf13f61-c5cc-4332-aab9-37405d1fdd14', + '8ceef7de-9408-48d1-8bb9-b8ac3ef73596', '7e053a85-b04c-484b-8815-d26ce646c748', + 'dfc09237-85b7-4596-b4e8-0bfbfd1aaeb7', 'd8d291ab-9dfa-45c0-bfc2-0a81167e3c63', + 'd0834157-d464-4632-b620-147e211e0614', '30758f35-fabc-48bc-b410-47450de64c82', + '7232bc80-a9cb-4668-9449-16b9282a2331', '29a194bd-2ecc-44bc-a6a9-6b8a25c3d860', + '52b43400-117a-4e16-81b8-b510fe987750', '52ef0c45-9ba1-4d5e-ba0c-82703196ec6c', + '9f0fa778-e6ae-4f7b-b4fd-2ccb1bd37eb7', '54b57afd-c91c-4d5f-8177-c3fe940bc723', + '23620661-6eb6-41df-9de4-18ff99e6f35f', 'c983fe89-6ac3-4395-882e-4b11aa11a99d', + 'd4d01494-e7d0-4a38-aa93-523a77855371', 'e5a77df6-fd29-48c7-8df6-f625de0179e7', + 'bca45eef-fc64-465f-9106-403814f180a0', '3b5c305d-be5e-4209-9ba2-c2d1620a5a4f', + '14c533a5-c15c-493b-8c7d-7c20a0bc5ea1', '7b3b1e40-c75d-4c2c-843b-1599c10123c2', + '54604482-72aa-4d64-a3bb-9d8dc4a38b41', 'baf6f609-2dce-4978-840b-b74261c5ba6d', + '250c536f-6cd9-4707-8706-b682961be032', 'a9b08140-ae44-409a-9941-22794c3fd097', + 'e516bd18-c46f-46b5-88dc-c7eac5a0f5a9', '6a220133-c71f-44df-8912-16f5b72568cb', + 'b63c2adf-b8f3-4e43-8651-44574475621f', '01d88287-387b-4a34-b6d3-0931aba8eb1a', + 'eb0cef3d-4e2b-45b3-88df-fc547a763d01', 'a79dee0f-8579-4e0a-b77f-0a468a3ca71f', + '62e50edb-03e6-4f95-a6f4-8ff3ef28a32b', '337fffe8-cc68-44b7-9636-2433f86ed5a9', + 'bd68f7b7-f8bb-489e-ac29-f8cbfe1978b3', '93d2a7d6-06a6-41bf-9632-c24e1f1c917a'] + rounds = 0 + for i in quiz_list: + rounds = rounds+1 + print(f"Got {rounds} from {len(quiz_list)}") + await get(i) diff --git a/classquiz/kahoot_importer/test_search.py b/classquiz/kahoot_importer/test_search.py new file mode 100644 index 0000000..69a3864 --- /dev/null +++ b/classquiz/kahoot_importer/test_search.py @@ -0,0 +1,20 @@ +from unittest import IsolatedAsyncioTestCase +from classquiz.kahoot_importer.search import search + + +class Test(IsolatedAsyncioTestCase): + async def test_search(self): + res = await search(query="Python", limit=100) + self.assertEqual(len(res.entities), 100) + await search(query="Test Quiz", limit=100) + await search(query="Biologie", limit=100) + await search(query="Chemie", limit=100) + await search(query="Deutsch", limit=100) + await search(query="Mathe", limit=100) + await search(query="Englisch", limit=100) + await search(query="Barbie", limit=100) + await search(query="Internet", limit=100) + await search(query="Windows", limit=100) + res2 = await search(query="Python", limit=100) + res2_ids = [str(e.card.uuid) for e in res2.entities] + print(res2_ids) diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index dbd5230..c4c1ef5 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -54,7 +54,7 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): @router.get("/join/{game_pin}") async def get_game_id(game_pin: str): - redis_res = (await redis.get(f"game:{game_pin}")).decode() + redis_res = (await redis.search(f"game:{game_pin}")).decode() if redis_res is None: raise HTTPException(status_code=404, detail="game not found") else: diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index c9109d8..70b3404 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -31,14 +31,14 @@ async def join_game(sid, data): if not resp_data["success"]: print("CAPTCHA FAILED") return - redis_res = await redis.get(f"game:{data['game_pin']}") + redis_res = await redis.search(f"game:{data['game_pin']}") if redis_res is None: await sio.emit("game_not_found", room=sid) else: session = {'game_pin': data["game_pin"], "username": data["username"], "admin": False} await sio.save_session(sid, session) await sio.emit("joined_game", redis_res, room=sid) - redis_res = (await redis.get(f"game_session:{data['game_pin']}")) + redis_res = (await redis.search(f"game_session:{data['game_pin']}")) redis_res = json.loads(redis_res) print(redis_res) redis_res["players"].append({"username": data["username"], "sid": sid}) @@ -62,11 +62,12 @@ async def start_game(sid, data): async def register_as_admin(sid, data): game_pin = data["game_pin"] game_id = data["game_id"] - if (await redis.get(f"game_session:{game_pin}")) is None: + if (await redis.search(f"game_session:{game_pin}")) is None: await redis.set(f"game_session:{game_pin}", json.dumps({"admin": sid, "game_id": game_id, "players": []}), ex=18000) - await sio.emit("registered_as_admin", {"game_id": game_id, "game": await redis.get(f"game:{data['game_pin']}")}, + await sio.emit("registered_as_admin", {"game_id": game_id, "game": await redis.search( + f"game:{data['game_pin']}")}, room=sid) async with sio.session(sid) as session: session['game_pin'] = data["game_pin"] @@ -80,7 +81,7 @@ async def register_as_admin(sid, data): async def get_question_results(sid, data): session = await sio.get_session(sid) if session["admin"]: - redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") + redis_res = await redis.search(f"game_session:{session['game_pin']}:{data['question_number']}") game_pin = session['game_pin'] await sio.emit("question_results", redis_res, room=game_pin) @@ -96,16 +97,16 @@ async def set_question_number(sid, data): @sio.event async def submit_answer(sid, data): session = await sio.get_session(sid) - redis_res = await redis.get(f"game_session:{session['game_pin']}") + redis_res = await redis.search(f"game_session:{session['game_pin']}") game_session = GameSession(**json.loads(redis_res)) - game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}"))) + game_data = PlayGame(**json.loads(await redis.search(f"game:{session['game_pin']}"))) print(game_session) answer_right = False for answer in game_data.questions[int(data["question_index"])].answers: if answer.answer == data["answer"] and answer.right: answer_right = True break - answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}") + answers = await redis.search(f"game_session:{session['game_pin']}:{data['question_index']}") if answers is None: await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", json.dumps( @@ -140,7 +141,7 @@ async def submit_answer(sid, data): @sio.event async def get_game_data(sid, data): game_pin = (await sio.get_session(sid))['game_pin'] - game_data = await redis.get(f"game:{game_pin}") + game_data = await redis.search(f"game:{game_pin}") if game_data is not None: await sio.emit("game_data", json.loads(game_data), room=game_pin) print(sid, data, "GET_GAME_DATA")