Merge remote-tracking branch 'origin/master'

This commit is contained in:
Mawoka
2022-04-24 20:46:57 +02:00
10 changed files with 62 additions and 27 deletions
+8 -8
View File
@@ -14,14 +14,14 @@
<p align='center'> <p align='center'>
The open-source quiz-platform! The open-source quiz-platform!
<br/> <br/>
<a href='https://classquiz.mawoka.eu/'><strong>Visit the website »</strong></a> <a href='https://classquiz.de/'><strong>Visit the website »</strong></a>
<br /> <br />
<br /> <br />
<a href='https://classquiz.mawoka.eu/docs'>Docs</a> <a href='https://classquiz.de/docs'>Docs</a>
· ·
<a href='https://classquiz.mawoka.eu/account/register'>Register</a> <a href='https://classquiz.de/account/register'>Register</a>
· ·
<a href='https://classquiz.mawoka.eu/docs/self-host'>Self-Hosting</a> <a href='https://classquiz.de/docs/self-host'>Self-Hosting</a>
</p> </p>
</div> </div>
@@ -33,21 +33,21 @@ quiz, so students can compete with their knowledge against each other.
## Try it ## Try it
There is a hosted version at [ClassQuiz.Mawoka.eu](https://classquiz.mawoka.eu?utm_medium=Github&utm_source=Readme). The There is a hosted version at [classquiz.de](https://classquiz.de?utm_medium=Github&utm_source=Readme). The
server is located in Karlsruhe, Germany and hosted by [netcup](https://mawoka.eu/redir?token=2), so expect some latency server is located in Karlsruhe, Germany and hosted by [netcup](https://mawoka.eu/redir?token=2), so expect some latency
depending on your location. depending on your location.
## Self-Host ## Self-Host
Please see https://classquiz.mawoka.eu/docs/self-host Please see https://classquiz.de/docs/self-host
## Development ## Development
See https://classquiz.mawoka.eu/docs/develop See https://classquiz.de/docs/develop
## Docs ## Docs
The docs are online at https://classquiz.mawoka.eu/docs The docs are online at https://classquiz.de/docs
### Things to know about the structure ### Things to know about the structure
+2 -1
View File
@@ -3,7 +3,7 @@ from fastapi import FastAPI, Request
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
from socketio import ASGIApp from socketio import ASGIApp
from classquiz.config import settings from classquiz.config import settings, meilisearch
from classquiz.db import database from classquiz.db import database
from classquiz.routers import users, quiz, utils, stats, storage, search from classquiz.routers import users, quiz, utils, stats, storage, search
from classquiz.socket_server import sio from classquiz.socket_server import sio
@@ -32,6 +32,7 @@ async def startup() -> None:
database_ = app.state.database database_ = app.state.database
if not database_.is_connected: if not database_.is_connected:
await database_.connect() await database_.connect()
meilisearch.index(settings.meilisearch_index).update_settings({"sortableAttributes": ["created_at"]})
@app.on_event("shutdown") @app.on_event("shutdown")
+2
View File
@@ -30,6 +30,8 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
return "quiz not found" return "quiz not found"
quiz_questions: list[dict] = [] quiz_questions: list[dict] = []
quiz_id = uuid.uuid4() quiz_id = uuid.uuid4()
meilisearch.delete_index(settings.meilisearch_index)
meilisearch.create_index(settings.meilisearch_index)
for q in quiz.kahoot.questions: for q in quiz.kahoot.questions:
answers: list[QuizAnswer] = [] answers: list[QuizAnswer] = []
+1
View File
@@ -37,6 +37,7 @@ async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_curren
raise HTTPException(status_code=400, detail="image url is not valid") raise HTTPException(status_code=400, detail="image url is not valid")
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4()) quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
await redis.delete("global_quiz_count") await redis.delete("global_quiz_count")
if quiz_input.public:
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)]) meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
return await quiz.save() return await quiz.save()
+23 -2
View File
@@ -16,11 +16,11 @@ from classquiz.auth import (
get_current_user, get_current_user,
) )
from classquiz.cache import clear_cache_for_account from classquiz.cache import clear_cache_for_account
from classquiz.config import redis, settings from classquiz.config import redis, settings, meilisearch
import uuid import uuid
import bleach import bleach
from pydantic import BaseModel from pydantic import BaseModel
from classquiz.db.models import User, UserSession, UpdatePassword, Token from classquiz.db.models import User, UserSession, UpdatePassword, Token, Quiz
from classquiz.emails import send_register_email, send_forgotten_password_email from classquiz.emails import send_register_email, send_forgotten_password_email
settings = settings() settings = settings()
@@ -259,3 +259,24 @@ async def delete_session(session_id: str, user: User = Depends(get_current_user)
async def get_session(user: User = Depends(get_current_user)): async def get_session(user: User = Depends(get_current_user)):
session = await UserSession.objects.filter(user=user).first() session = await UserSession.objects.filter(user=user).first()
return session return session
class DeleteUserInput(BaseModel):
password: str
@router.delete("/me")
async def delete_user_account(input_data: DeleteUserInput, user: User = Depends(get_current_user)):
if not verify_password(input_data.password, user.password):
raise HTTPException(status_code=400, detail="Incorrect password")
user = await User.objects.filter(id=user.id).get_or_none()
await UserSession.objects.filter(user=user).delete()
quizzes = await Quiz.objects.filter(user_id=user).all()
quizzes_to_delete = []
for quiz in quizzes:
if quiz.is_public:
quizzes_to_delete.append(quiz.id)
if len(quizzes_to_delete) > 0:
meilisearch.index(settings.meilisearch_index).delete_documents(quizzes_to_delete)
await User.objects.filter(id=user.id).delete()
await user.delete()
+1 -1
View File
@@ -3,7 +3,7 @@ import pytest
from classquiz.kahoot_importer.get import get from classquiz.kahoot_importer.get import get
@pytest.mark.order("last") @pytest.mark.order(-2)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get(): async def test_get():
await get("1f95eb0b-fcf4-4db2-879b-5418ef75116b") await get("1f95eb0b-fcf4-4db2-879b-5418ef75116b")
+1 -1
View File
@@ -4,7 +4,7 @@ from classquiz.kahoot_importer.search import search
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.order("last") @pytest.mark.order(-2)
async def test_search(): async def test_search():
res = await search(query="Python", limit=100) res = await search(query="Python", limit=100)
assert len(res.entities) == 100 assert len(res.entities) == 100
+13
View File
@@ -359,3 +359,16 @@ class TestQuiz:
ValueStorage.imported_quizzes.append(resp.json()["id"]) ValueStorage.imported_quizzes.append(resp.json()["id"])
resp = test_client.post("/api/v1/quiz/import/1f95eb0bdassdadasdas", cookies={"access_token": token}) resp = test_client.post("/api/v1/quiz/import/1f95eb0bdassdadasdas", cookies={"access_token": token})
assert resp.text == '"quiz not found"' assert resp.text == '"quiz not found"'
class TestDeleteUser:
@pytest.mark.asyncio
async def test_delete_user(self, test_client):
resp = test_client.post(
"/api/v1/users/token/cookie", data={"username": test_user_email, "password": test_user_password}
)
token = resp.cookies["access_token"]
resp = test_client.delete(
"/api/v1/users/me", cookies={"access_token": token}, json={"password": test_user_password}
)
assert resp.status_code == 200
+9 -12
View File
@@ -31,29 +31,26 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td> Your IP-Address </td> <td> The IP-Address of the user</td>
<td> <td>
<ul> <ul>
<li>
I store them because I am too lazy to configure them to be not stored.
</li>
<li>To prevent abuse</li> <li>To prevent abuse</li>
<li>To provide user-sessions</li> <li>To provide user-sessions</li>
</ul> </ul>
</td> </td>
<td> For a very long time </td> <td> For a very long time</td>
<td> On my server, by Netcup, in Germany. </td> <td> On the developers server, by Netcup, in Germany.</td>
</tr> </tr>
<tr> <tr>
<td> The data you enter (quizzes, email, username) </td> <td> The data the user enters (quizzes, email, username)</td>
<td> Self-explaining </td> <td> Self-explaining</td>
<td> Untill you delete your account </td> <td> Maximum of 30 days after you've deleted your account</td>
<td> On my server, by Netcup, in Germany. </td> <td> On the developers server, by Netcup, in Germany.</td>
</tr> </tr>
<tr> <tr>
<td>Your hashed password</td> <td>The <b>hashed</b> password of the user</td>
<td>Self-explaining</td> <td>Self-explaining</td>
<td>Untill you delete your account</td> <td>Maximum of 30 days after you've deleted your account</td>
<td>On my server, by Netcup, in Germany.</td> <td>On my server, by Netcup, in Germany.</td>
</tr> </tr>
</tbody> </tbody>
+1 -1
View File
@@ -15,7 +15,7 @@ async def __main__():
quizzes = await Quiz.objects.filter(public=True).all() quizzes = await Quiz.objects.filter(public=True).all()
for quiz in quizzes: for quiz in quizzes:
meili_data.append(await get_meili_data(quiz)) meili_data.append(await get_meili_data(quiz))
print(meili_data) print(len(meili_data))
client = meilisearch.Client(settings.meilisearch_url) client = meilisearch.Client(settings.meilisearch_url)
client.index(settings.meilisearch_index).add_documents(meili_data) client.index(settings.meilisearch_index).add_documents(meili_data)
client.index(settings.meilisearch_index).update_settings({"sortableAttributes": ["created_at"]}) client.index(settings.meilisearch_index).update_settings({"sortableAttributes": ["created_at"]})