Compare commits

..

1 Commits

Author SHA1 Message Date
4fb13e9136 Footer immer Unten 2025-04-05 12:43:49 +02:00
150 changed files with 836 additions and 13141 deletions

2
.gitignore vendored
View File

@@ -1,10 +1,8 @@
__pycache__
.venv
.env
db.sqlite3
tailwindcss.exe
t-style.css
dump.rdb
media
node_modules
staticfiles

View File

@@ -7,7 +7,6 @@
3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts
@@ -41,69 +40,23 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
```
7. Die Datenbank migrieren:
```sh
python django/manage.py migrate
python3 core/manage.py migrate
```
## Starten des Servers
### Entwicklungsserver starten
Für die Entwicklung mit WebSocket-Unterstützung verwenden wir Daphne:
Um den Server zu starten, verwende einen der folgenden Befehle:
1. Aktiviere die virtuelle Umgebung:
```sh
source .venv/bin/activate # Linux
.venv\Scripts\activate # Windows
```
2. Starte den Daphne-Server:
```sh
# Im Verzeichnis 'django'
daphne -b 127.0.0.1 -p 8000 core.asgi:application
```
3. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Alternativ kannst du den Django-Entwicklungsserver verwenden, wenn du keine WebSocket-Funktionalität benötigst:
```sh
python3 core/manage.py runserver
```
### Produktionsserver starten
Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP als auch WebSocket-Verbindungen unterstützt:
1. Aktiviere die virtuelle Umgebung wie oben beschrieben
2. Sammle die statischen Dateien:
```sh
python3 core/manage.py collectstatic
```
3. Starte den Daphne-Server:
```sh
daphne -b 0.0.0.0 -p 8000 core.asgi:application
```
- `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces
- `-p 8000`: Port (anpassbar)
4. Cronjob für Bereinigung der Spiele in der Datenbank
- Zum bearbeiten der Cronjobs:
```sh
crontab -e
```
- Dann `0 0 * * * /path/to/venv/bin/python /path/to/your_project/manage.py cleanup_games` einfügen, damit um Mitternacht alle Spiele gelöscht werden
5. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
- Standardport (8000):
```sh
python3 core/manage.py runserver
```
- Bestimmten Port (z. B. 9000):
```sh
python3 core/manage.py runserver 9000
```
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
@@ -147,10 +100,10 @@ npx @tailwindcss/cli -i <INPUT-DATEI> -o <OUTPUT-DATEI> --watch --minify
```sh
# unter Linux mit Tailwind Binärdatei:
npx tailwindcss -i static/css/t-input.css -o static/css/t-style.css --watch --minify
tailwindcss -i static/css/t-input.css -o static/css/t-style.css --watch --minify
# unter Windows mit NPM (in bash mit '/' statt '\'):
npx @tailwindcss/cli -i ./static/css/t-input.css -o ./static/css/t-style.css --watch --minify
npx @tailwindcss/cli -i .\static\css\t-input.css -o .\static\css\t-style.css --watch --minify
```
## Issue - Merge Request - Merge
@@ -159,23 +112,6 @@ Hier der vereinbarte Arbeitsablauf:
1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird.
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
2. Merge Request anlegen - dadurch wird der Bransch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
## Konfiguration
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen |
| --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
## Lizenzen für node_modules
1. Installation: npm install license-checker --save-dev
npx license-checker --markdown > THIRD_PARTY_LICENSES.md
## für Python Packete:
pip-licenses --format=markdown > THIRD_PARTY_LICENSES_PYTHON.md

View File

@@ -1,7 +0,0 @@
Name und Anschrift des Verantwortlichen
//NAME HIER EINSETZEN//
//ADRESSE HIER EINSETZEN//
//E-MAIL HIER EINSETZEN//

Binary file not shown.

View File

@@ -1,44 +0,0 @@
| Name | Version | License |
|--------------------|--------------|---------------------------------------------------------|
| Automat | 25.4.16 | MIT License |
| Django | 5.1.13 | BSD License |
| Twisted | 25.5.0 | MIT License |
| asgiref | 3.10.0 | BSD License |
| attrs | 25.4.0 | UNKNOWN |
| autobahn | 25.10.1 | UNKNOWN |
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
| cffi | 2.0.0 | UNKNOWN |
| channels | 4.2.2 | BSD License |
| channels_redis | 4.3.0 | BSD |
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
| charset-normalizer | 3.4.4 | MIT |
| colorama | 0.4.6 | BSD License |
| constantly | 23.10.4 | MIT License |
| cryptography | 46.0.3 | UNKNOWN |
| daphne | 4.1.2 | BSD License |
| django-cleanup | 9.0.0 | MIT License |
| geographiclib | 2.1 | MIT |
| geopy | 2.4.1 | MIT License |
| hyperlink | 21.0.0 | MIT License |
| idna | 3.11 | UNKNOWN |
| incremental | 24.7.2 | MIT License |
| msgpack | 1.1.2 | UNKNOWN |
| pillow | 11.1.0 | CMU License (MIT-CMU) |
| pyOpenSSL | 25.3.0 | Apache Software License |
| pyasn1 | 0.6.1 | BSD License |
| pyasn1_modules | 0.4.2 | BSD License |
| pycparser | 2.23 | BSD License |
| pypng | 0.20220715.0 | MIT License |
| python-dotenv | 1.1.1 | BSD License |
| qrcode | 7.4.2 | BSD License |
| redis | 6.4.0 | MIT License |
| reportlab | 4.2.5 | BSD License |
| requests | 2.32.5 | Apache Software License |
| service-identity | 24.2.0 | MIT License |
| sqlparse | 0.5.3 | BSD License |
| txaio | 25.9.2 | UNKNOWN |
| typing_extensions | 4.15.0 | UNKNOWN |
| tzdata | 2025.2 | Apache Software License |
| urllib3 | 2.5.0 | UNKNOWN |
| whitenoise | 6.6.0 | MIT License |
| zope.interface | 8.0.1 | Zope Public License |

View File

@@ -6,15 +6,11 @@ from django import forms
class RegisterForm(UserCreationForm):
class Meta:
model=User
fields = ['username','password1','password2', "email"]
fields = ['username','password1','password2']
username = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
)
email = forms.EmailField(
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
)
password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'})
)
@@ -23,3 +19,4 @@ class RegisterForm(UserCreationForm):
)

View File

@@ -1,8 +1,7 @@
from django.urls import path, reverse_lazy # type: ignore
from django.urls import path # type: ignore
from . import views
from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import LoginView
from django.contrib.auth import views as auth_views
app_name = 'accounts' # Namensraum zum verhindern von Konflikten zwischen Apps
#
@@ -14,22 +13,4 @@ urlpatterns = [
path('logout/', LogoutView.as_view(next_page='accounts:login'), name='logout'),
path('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'),
path('register/', views.register, name='register'),
path('password_reset/', auth_views.PasswordResetView.as_view(success_url=reverse_lazy('accounts:password_reset_done')), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('password_change/', views.password_changed, name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('delete_account/', views.deleteAccount, name="delete_account"),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html',
success_url=reverse_lazy('accounts:password_reset_complete')
),
name='password_reset_confirm'
),
]

View File

@@ -2,25 +2,11 @@ from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from .forms import RegisterForm
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.contrib import messages
from django.contrib.auth import logout
from library.models import QivipQuiz, QivipQuestion
# Create your views here.
@login_required
def home(request):
my_quizzes_number=QivipQuiz.objects.filter(user_id=request.user).count()
my_questions_number = QivipQuestion.objects.filter(quiz_id__user_id=request.user).count()
context = {
'my_quizzes_number': my_quizzes_number,
'my_questions_number': my_questions_number,
}
return render(request, 'accounts/home.html', context=context)
return render(request, 'accounts/home.html')
def register(response):
if response.method == "POST":
@@ -33,33 +19,3 @@ def register(response):
form = RegisterForm()
return render(response, "accounts/register.html", {"form":form})
from django.contrib.auth import views as auth_views
@login_required
def password_changed(request):
if request.method == 'POST':
form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
user = form.save()
#update_session_auth_hash(request, user) #Hier wäre der User nach Passwortänderung noch angemeldet!
logout(request)
messages.success(request, "Das Passwort wurde erfolgreich geändert!")
return redirect('accounts:home')
else:
form = PasswordChangeForm(user=request.user)
return render(request, 'registration/password_change_form.html', {'form': form})
@login_required
def deleteAccount(request):
if request.method == 'POST':
user=request.user
logout(request)
user.delete()
messages.success(request, "Der Account wurde erfolgreich gelöscht!")
return redirect('homepage:home')
return render(request, 'registration/delete_account_confirm.html')

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ComponentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'components'

View File

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
django/components/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path # type: ignore
from . import views
app_name = 'components' # Namensraum zum verhindern von Konflikten zwischen Apps
#
# Wichtig: Damit Links funktionieren müssen diese so eingebunden werden: {% url 'accounts:login' %} statt {% url 'login' %}
#
urlpatterns = [
path('<int:pk>', views.play, name='play'),
path('ranking/<int:pk>', views.show_rank, name='ranking')
]

View File

@@ -0,0 +1,9 @@
from django.shortcuts import render
# Create your views here.
def play(request,pk): #
return render(request, 'components/answer.html', {"pk": pk}) #
def show_rank(request,pk): #
return render(request, 'components/ranking.html', {"pk": pk}) #

View File

@@ -1,3 +0,0 @@
{
"ENABLE_RATING_SYSTEM": true
}

View File

@@ -8,21 +8,9 @@ https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django.setup()
from play.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})
application = get_asgi_application()

View File

@@ -10,42 +10,30 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import json, os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR ENTWICKLUNG
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# Support env variables from .env file if defined
env_path = load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(env_path)
# SECURITY WARNING: keep the secret key used in production secret!
#SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87')
SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
#DEBUG = False
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
# Konfigurationsdatei für Projekt
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
QIVIP_CONFIG = json.load(config_file)
# Application definition
INSTALLED_APPS = [
'library.apps.LibraryConfig',
'homepage.apps.HomepageConfig',
'components.apps.ComponentsConfig',
'accounts.apps.AccountsConfig',
'play',
'django.contrib.admin',
@@ -54,14 +42,10 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'django_cleanup',
'django.contrib.humanize',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -89,17 +73,6 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6380)],
},
},
}
# Database
@@ -112,15 +85,6 @@ DATABASES = {
}
}
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -157,15 +121,8 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
@@ -176,13 +133,3 @@ import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien
# Email Settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'postoffice.katharineum.de'
EMAIL_PORT = 587 # 25
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'qivip-mail@katharineum.de'
EMAIL_HOST_PASSWORD = '<password>'
EMAIL_USE_SSL = False
DEFAULT_FROM_EMAIL = 'qivip-mail@katharineum.de'

View File

@@ -14,8 +14,6 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.conf import settings
from django.contrib import admin # type: ignore
from django.urls import path, include
@@ -25,7 +23,5 @@ urlpatterns = [
path('', include('homepage.urls')),
path('play/', include('play.urls')),
path('library/', include('library.urls')),
path('components/', include('components.urls'))
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,8 +1,3 @@
from django.contrib import admin
from .models import QivipFaq
# Register your models here.
class QivipFaqAdmin(admin.ModelAdmin):
list_display = ('question','answer',)
# Registrierung der Modelle im Admin
admin.site.register(QivipFaq, QivipFaqAdmin)
# Register your models here.

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-01 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='QivipFAQs',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=200)),
('answer', models.CharField(max_length=1000)),
],
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-01 17:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('homepage', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='QivipFAQs',
new_name='QivipFaq',
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-01 17:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homepage', '0002_rename_qivipfaqs_qivipfaq'),
]
operations = [
migrations.AlterField(
model_name='qivipfaq',
name='answer',
field=models.TextField(max_length=1000),
),
migrations.AlterField(
model_name='qivipfaq',
name='question',
field=models.TextField(max_length=200),
),
]

View File

@@ -1,6 +1,3 @@
from django.db import models
# Create your models here.
class QivipFaq(models.Model):
question = models.TextField(max_length=200)
answer = models.TextField(max_length=1000)

View File

@@ -1,10 +1,7 @@
from django.shortcuts import render, redirect
from .models import QivipFaq
def home(request):
faqs=QivipFaq.objects.all()
context={"faqs": faqs}
return render(request, 'homepage/home.html', context)
return render(request, 'homepage/home.html')
def impress(request):
return render(request, 'homepage/impress.html')

View File

@@ -1,14 +1,15 @@
from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating
from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag
# Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
list_display = ('name', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen
# Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date')
search_fields = ('data',)
list_filter = ('quiz_id',)
@@ -18,47 +19,13 @@ class QuizCategoryAdmin(admin.ModelAdmin):
search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
class QuizImageAdmin(admin.ModelAdmin):
list_display = ('image',)
class QuestionImageAdmin(admin.ModelAdmin):
list_display = ('image',)
from django import forms
from .models import QuizCategory
class QuizCategoryForm(forms.ModelForm):
color = forms.CharField(
widget=forms.TextInput(attrs={'type': 'color'})
)
class Meta:
model = QuizCategory
fields = '__all__'
from django.utils.html import format_html
@admin.register(QuizCategory)
class QuizCategoryAdmin(admin.ModelAdmin):
form = QuizCategoryForm
list_display = ('name', 'color_preview', 'color')
def color_preview(self, obj):
return format_html(
'<div style="width: 24px; height: 24px; background-color: {}; border: 2px solid #000;"></div>',
obj.color
)
color_preview.short_description = "Farbe"
# Für das Tag Modell
class TagAdmin(admin.ModelAdmin):
list_display = ('name',) # Zeige nur den Namen des Tags
search_fields = ('name',)
# Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin)
#admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(Tag, TagAdmin)

View File

@@ -1,47 +1,31 @@
from django import forms
from .models import QivipQuiz, QivipQuestion, QuizCategory
from .models import QivipQuiz, QivipQuestion
class QuizForm(forms.ModelForm):
class Meta:
model = QivipQuiz
fields = ['name', 'description', 'status', 'category','difficulty','credits']
def __init__(self, *args, **kwargs):
super(QuizForm, self).__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({
'class': 'dark:bg-[#2a2f3a]! dark:text-white!'
})
fields = ['name', 'description','image', 'status', 'category', 'tags',"difficulty"]
class QuestionForm(forms.ModelForm):
class Meta:
model = QivipQuestion
fields = ['quiz_id', 'data','time_per_question']
fields = ['quiz_id', 'data']
from django import forms
class QuizFilterForm(forms.Form):
search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'list': 'quiz-names',
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...',
}))
min_amount_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
min_amout_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
max_amount_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
max_amout_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
categories = forms.ModelChoiceField(
queryset=QuizCategory.objects.filter().distinct(),
required=False,
label="Kategorie",
widget=forms.Select(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full dark:bg-[#2a2f3a] dark:text-white'
}))

View File

@@ -1,13 +0,0 @@
from django.core.management.base import BaseCommand
from play.models import QuizGame
from django.utils import timezone
class Command(BaseCommand):
help = 'Bereinigt beendete Spiele'
def handle(self, *args, **kwargs):
expired_games = QuizGame.objects.filter(
updated_at__lt=timezone.now() - timezone.timedelta(minutes=5))
deleted_count = expired_games.count()
expired_games.delete()
self.stdout.write(f'{deleted_count} alte Spiele gelöscht.')

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-03 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0024_alter_qivipquiz_image'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, default='Das Quiz wurde modifiziert von:', editable=False),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0025_qivipquiz_modified_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, editable=False),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0026_alter_qivipquiz_modified_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0027_qivipquiz_creator'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 16:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0028_alter_qivipquiz_creator'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 09:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0029_qivipquiz_credits'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='creator',
),
migrations.RemoveField(
model_name='qivipquiz',
name='modified_description',
),
migrations.AddField(
model_name='qivipquiz',
name='base_quiz',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_quizzes', to='library.qivipquiz'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 17:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0030_remove_qivipquiz_creator_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='quiz_id',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.qivipquiz'),
),
]

View File

@@ -1,37 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 19:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0031_alter_qivipquestion_quiz_id'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='average_rating',
field=models.FloatField(default=3.0),
),
migrations.AddField(
model_name='qivipquiz',
name='rating_count',
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name='QuizRating',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('participant_id', models.CharField(max_length=200)),
('rating', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')])),
('created_at', models.DateTimeField(auto_now_add=True)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='library.qivipquiz')),
],
options={
'unique_together': {('quiz', 'participant_id')},
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180), ('nicht gesetzt', 'nicht gesetzt')], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 13:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='tags',
),
migrations.DeleteModel(
name='Tag',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_remove_qivipquiz_tags_delete_tag'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='status',
field=models.CharField(choices=[('öffentlich', 'Öffentlich'), ('privat', 'Privat')], default='öffentlich', max_length=10),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=3),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquiz_status'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,32 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 16:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0035_qivipquiz_favorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
migrations.CreateModel(
name='QuizFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('favorite', models.BooleanField(default=False)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.qivipquiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'quiz')},
},
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 16:05
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0036_remove_qivipquiz_favorite_quizfavorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='QuizFavorite',
new_name='QivipQuizFavorite',
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 17:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0037_rename_quizfavorite_qivipquizfavorite'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='quiz',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='library.qivipquiz'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0038_alter_qivipquizfavorite_quiz'),
]
operations = [
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:15
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0039_qivipquizfavorite_favorite_date'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description], verbose_name='Beschreibung'),
),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0040_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='library.quizcategory', verbose_name='Kategorie'),
),
migrations.AlterField(
model_name='qivipquiz',
name='difficulty',
field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13, verbose_name='Schwierigkeit'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=75, verbose_name='Name des Quizzes'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.qivipquizfavorite'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 15:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0042_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0043_remove_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquizfavorite',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquiz',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0044_remove_qivipquizfavorite_favorite_date_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0045_remove_qivipquiz_favorite_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,14 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0035_alter_qivipquestion_time_per_question'),
('library', '0046_alter_qivipquizfavorite_favorite_date'),
]
operations = [
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0047_merge_20250418_1334'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, height_field=900, max_length=200, null=True, upload_to='question_images/', verbose_name='Bild', width_field=1000),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0048_qivipquestion_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-22 16:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0049_alter_qivipquestion_image'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.QuizImage'),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 12:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0050_questionimage_quizimages_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0051_alter_qivipquestion_image_alter_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.DeleteModel(
name='QuestionImage',
),
migrations.DeleteModel(
name='QuizImage',
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 13:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0052_alter_qivipquestion_image_alter_qivipquiz_image_and_more'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-13 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0053_questionimage_quizimage_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-26 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0054_qivipquestion_credits'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='published_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-06-27 12:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0055_qivipquiz_published_at'),
]
operations = [
migrations.AlterUniqueTogether(
name='quizrating',
unique_together=set(),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-07-02 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0056_alter_quizrating_unique_together'),
]
operations = [
migrations.AddField(
model_name='quizcategory',
name='color',
field=models.CharField(default='#000000', help_text='Farbe als Hex-Code, z.B. #ff0000', max_length=7),
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0057_quizcategory_color'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='qivipquiz',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0058_qivipquiz_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='single_player_or_multiple',
),
]

View File

@@ -3,20 +3,11 @@ from django.contrib.auth.models import User
from django.utils.text import slugify
import uuid
from django.core.exceptions import ValidationError
from django.utils import timezone
class QuizImage(models.Model):
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
class QuestionImage(models.Model):
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
class QivipQuiz(models.Model):
STATUS_VALUES = {
"öffentlich": "Öffentlich",
#"versteckt": "Versteckt",
"versteckt": "Versteckt",
"privat": "Privat",
}
DIFFICULTY_VALUES = {
@@ -29,135 +20,47 @@ class QivipQuiz(models.Model):
}
def validate_description(value):
if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ForeignKey(
QuizImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="quizzes"
)
quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus= models.IntegerField(null=True, blank=True, default=0)
quiz_played_Moderiermodus_players= models.IntegerField(null=True, blank=True, default=0)
image = models.ImageField(max_length=256, upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
base_quiz = models.ForeignKey('self', on_delete=models.SET_NULL,blank=True, null=True, related_name='child_quizzes')
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich")
category = models.ForeignKey('QuizCategory',verbose_name="Kategorie", related_name='quiz', on_delete=models.CASCADE)
name = models.CharField(max_length=75,verbose_name="Name des Quizzes")
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...", verbose_name="Beschreibung")
difficulty= models.CharField(verbose_name="Schwierigkeit", max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
credits=models.TextField(blank=True, editable=True)
average_rating = models.FloatField(default=3.0)
rating_count = models.IntegerField(default=0)
published_at = models.DateTimeField(null=True, blank=True) # manuell gesetzt, wenn veröffentlicht
category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
name = models.CharField(max_length=75)
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...")
difficulty= models.CharField(max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
def is_published(self):
return self.published_at is not None and self.published_at <= timezone.now()
def __str__(self):
return self.name
class QivipQuizFavorite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # Verknüpfung zu User
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='favorites') # Verknüpfung zu Quiz
favorite = models.BooleanField(default=False) # Favoritenstatus
favorite_date = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
# Create your models here.
class QivipQuestion(models.Model):
TIME_VALUES = {
"10": 10,
"15": 15,
"30": 30,
"60": 60,
"90": 90,
"120": 120,
"180": 180,
}
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions')
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='question')
creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True)
data = models.TextField()
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ForeignKey(
QuestionImage,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="questions"
)
credits=models.TextField(blank=True, editable=True)
def __str__(self):
return self.data[:50]
class Tag(models.Model):
name = models.CharField(max_length=100, unique=True)
class QuizRating(models.Model):
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='ratings')
participant_id = models.CharField(max_length=200)
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
created_at = models.DateTimeField(auto_now_add=True)
#class Meta:
#unique_together = ('quiz', 'participant_id')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Update quiz average rating (immer, auch bei Updates)
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()
"""
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Update quiz average rating
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()"""
def __str__(self):
return self.name
class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True)
slug = models.SlugField(unique=True, blank=True)
color= models.CharField(
max_length=7,
default="#000000",
help_text="Farbe als Hex-Code, z.B. #ff0000"
)
def save(self, *args, **kwargs):
if not self.slug:

View File

@@ -7,9 +7,6 @@ app_name = 'library'
urlpatterns = [
path('', views.overview_quiz, name='overview_quiz'),
path('my/', views.overview_quiz_my, name='overview_quiz_my'),
path('favorites/', views.overview_quiz_favorites, name='overview_quiz_favorites'),
path('new/', views.new_quiz, name='new_quiz'),
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
@@ -17,10 +14,6 @@ urlpatterns = [
path('question/new/', views.new_question, name='new_question'),
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
#path('quiz/<int:pk>/pdf/', views.pdf_solution, name='quiz_solution'),
#path('quiz/<int:pk>/pdf/task', views.pdf_task, name='quiz_task'),
]
]# Nur für die Entwicklungsumgebung
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

File diff suppressed because it is too large Load Diff

1486
django/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,6 @@
"license": "ISC",
"description": "",
"dependencies": {
"@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
"swiper": "^11.2.6"
}
}

View File

@@ -1,528 +0,0 @@
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from datetime import timedelta
from django.urls import reverse
from .models import QuizGame, QuizGameParticipant
class GameConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.game_group_name = f'game_{self.join_code}'
# Join game group
await self.channel_layer.group_add(
self.game_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
self.game_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_answer',
'answer': answer
}
)
elif message_type == 'get_answer_stats':
stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({
'type': 'answer_stats',
'stats': stats
}))
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'next_question':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.finish_game()
elif message_type == 'advance_to_scores':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scores()
async def participant_answer(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_answer',
'answer': event['answer']
}))
async def participant_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_list_update',
'participants': event['participants']
}))
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def verify_host(self, host_id):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
return quiz_game.host_id == host_id
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def save_answer(self, participant_id, answer_index, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
# Check if answer is correct
is_correct = False
if answer_index >= 0: # -1 means timeout
is_correct = current_question.data['options'][answer_index]['is_correct']
# Calculate score based on correctness and time
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / 30000 # 30 seconds max
score = int(base_score * (0.5 + 0.5 * time_factor))
participant.last_score=score
participant.score += score
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def get_answer_stats(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
# Count answers for each option
stats = {}
for participant in participants:
if participant.last_answer is not None:
stats[participant.last_answer] = stats.get(participant.last_answer, 0) + 1
return stats
except QuizGame.DoesNotExist:
return {}
@database_sync_to_async
def get_participants(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
return [
{
'id': p.participant_id,
'display_name': p.display_name,
'score': p.score
}
for p in participants
]
except QuizGame.DoesNotExist:
return []
@database_sync_to_async
def _advance_to_next_question(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz = quiz_game.quiz_id
# Move to next question
quiz_game.current_question_index += 1
# Check if we've reached the end
if quiz_game.current_question_index >= quiz.questions.count():
quiz_game.current_state = 'finished'
quiz_game.save()
return 'finished'
else:
# Update game state and start time
quiz_game.current_state = 'question'
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return 'question'
except QuizGame.DoesNotExist:
return None
async def advance_to_next_question(self):
result = await self._advance_to_next_question()
if result == 'finished':
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
elif result == 'question':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'next_question',
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scores'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scores(self):
success = await self._show_scores()
if success:
# Notify clients to redirect to scores page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scores',
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _finish_game(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def finish_game(self):
success = await self._finish_game()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
class LobbyConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.room_group_name = f'game_{self.join_code}'
self.heartbeat_task = None
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Start heartbeat check
self.heartbeat_task = asyncio.create_task(self.check_participants_heartbeat())
# Send current participants list
participants = await self.get_participants()
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
async def disconnect(self, close_code):
# Cancel heartbeat task
if self.heartbeat_task:
self.heartbeat_task.cancel()
try:
await self.heartbeat_task
except asyncio.CancelledError:
pass
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
async def game_state_update(self, event):
# Send game state update to WebSocket
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
elif message_type == 'submit_answer':
participant_id = text_data_json.get('participant_id')
answer = text_data_json.get('answer')
time_remaining = text_data_json.get('time_remaining')
if participant_id:
score = await self._process_answer(participant_id, answer, time_remaining)
await self.send(text_data=json.dumps({
'type': 'answer_processed',
'score': score
}))
elif message_type == 'next_question':
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
next_state = await self._advance_game_state()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': next_state['action'],
'redirect_url': next_state['redirect_url']
}
)
async def participant_list_update(self, event):
participants = event['participants']
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
@database_sync_to_async
def get_participants(self):
game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=game)
return [{'id': str(p.participant_id), 'name': p.display_name} for p in participants]
@database_sync_to_async
def _update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
participant.save() # This will update last_heartbeat due to auto_now=True
except QuizGameParticipant.DoesNotExist:
pass
@database_sync_to_async
def _is_host(self, host_id):
try:
return QuizGame.objects.filter(join_code=self.join_code, host_id=host_id).exists()
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
@database_sync_to_async
def _process_answer(self, participant_id, answer, time_remaining):
game = QuizGame.objects.get(join_code=self.join_code)
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
question = game.quiz_id.questions.all()[game.current_question_index]
# Load question data
question_data = json.loads(question.data)
correct_answer = question_data.get('correct_answer')
# Calculate score based on correctness and time
score = 0
if answer == correct_answer:
# Base score for correct answer + bonus for speed
score = 1000 + int(time_remaining * 10) # 10 points per remaining second
participant.last_score= score
participant.score += score
participant.save()
return score
@database_sync_to_async
def _advance_game_state(self):
game = QuizGame.objects.get(join_code=self.join_code)
total_questions = game.quiz_id.questions.count()
if game.current_state == 'question':
game.current_state = 'scores'
game.save()
return {
'action': 'show_scores',
'redirect_url': f'/play/game/{self.join_code}/scores'
}
elif game.current_state == 'scores':
if game.current_question_index + 1 < total_questions:
game.current_state = 'question'
game.current_question_index += 1
game.question_start_time = timezone.now()
game.save()
return {
'action': 'next_question',
'redirect_url': f'/play/game/{self.join_code}/{game.current_question_index}'
}
else:
game.current_state = 'finished'
game.save()
return {
'action': 'game_finished',
'redirect_url': f'/play/game/{self.join_code}/finished'
}
@database_sync_to_async
def _remove_inactive_participants(self):
# Remove participants who haven't sent a heartbeat in the last 30 seconds
timeout = timezone.now() - timedelta(seconds=30)
quiz_game = QuizGame.objects.get(join_code=self.join_code)
QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
last_heartbeat__lt=timeout
).delete()
async def check_participants_heartbeat(self):
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds
await self._remove_inactive_participants()
# Send updated participant list
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in heartbeat check: {e}')
await asyncio.sleep(10) # Wait before retrying

View File

@@ -1,3 +0,0 @@
from .game import GameConsumer
__all__ = ['GameConsumer']

View File

@@ -1,721 +0,0 @@
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from django.urls import reverse
from play.models import QuizGame, QuizGameParticipant, QuizAnswer,QivipQuiz
from django.conf import settings
from geopy.distance import geodesic
class GameConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.game_group_name = f'game_{self.join_code}'
# Join game group
await self.channel_layer.group_add(
self.game_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
self.game_group_name,
self.channel_name
)
# Check if game should be deleted
await self.cleanup_game()
@database_sync_to_async
def get_question_data(self):
quiz_game = QuizGame.objects.get(join_code=self.join_code)
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
return json.loads(current_question.data)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'ping':
# Respond with pong to keep connection alive
await self.send(text_data=json.dumps({
'type': 'pong'
}))
return
if message_type == 'leave_game':
participant_id = text_data_json.get('participant_id')
if participant_id:
try:
# Get participant info before deletion
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(participant_id=participant_id)
display_name = participant.display_name
join_code = self.join_code
# Delete participant
await database_sync_to_async(participant.delete)()
# Update participants list
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
# Send redirect to home
await self.send(text_data=json.dumps({
'type': 'redirect',
'url': '/'
}))
except QuizGameParticipant.DoesNotExist:
pass
return
if message_type == 'submit_rating':
participant_id = text_data_json['participant_id']
rating = text_data_json['rating']
success = await self.save_rating(participant_id, rating)
if success:
await self.send(text_data=json.dumps({
'type': 'rating_confirmed'
}))
elif message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0)
question_data = await self.get_question_data()
if question_data['type'] == "map":
await self.save_answer_map(participant_id, answer, time_remaining)
else:
await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_answer',
'answer': answer
}
)
elif message_type == 'start_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'get_answer_stats':
stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({
'type': 'answer_stats',
'stats': stats,
}))
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'next_question':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'winner_podest':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_winner_podest()
elif message_type == 'scoreboard':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scoreboard()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.finish_game()
elif message_type == 'advance_to_scores':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scores()
async def participant_answer(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_answer',
'answer': event['answer']
}))
async def participant_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_list_update',
'participants': event['participants']
}))
async def player_left(self, event):
await self.send(text_data=json.dumps({
'type': 'player_left',
'player_name': event['player_name'],
'was_kicked': event.get('was_kicked', False)
}))
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def verify_host(self, host_id):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
return quiz_game.host_id == host_id
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def save_answer_map(self, participant_id, answer_location, time_remaining):
answer_index = 0
try:
# Get all necessary data first
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
target_location = question_data['target_location']
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
# Parse coordinates if they are strings
def parse_coords(coord):
if isinstance(coord, str):
try:
# Handle both comma and space separated coordinates
if ',' in coord:
lat, lng = map(float, [x.strip() for x in coord.split(',')])
else:
# Try splitting by space if no comma
parts = coord.strip().split()
if len(parts) >= 2:
lat, lng = map(float, parts[:2])
else:
raise ValueError("Invalid coordinate format")
return (lat, lng)
except (ValueError, AttributeError) as e:
print(f"Error parsing coordinates {coord}: {e}")
return None
elif isinstance(coord, (list, tuple)) and len(coord) >= 2:
return (float(coord[0]), float(coord[1]))
return None
# Parse the coordinates
target_location = parse_coords(target_location)
answer_location_coords = answer_location
answer_location = parse_coords(answer_location)
if not target_location or not answer_location:
print("Invalid coordinates, cannot calculate score")
is_correct = False
score = 0
else:
# Configuration - Make these easily adjustable
MAX_DISTANCE_KM = 50 # Maximum distance for scoring (in km)
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
try:
# Calculate distance
distance_km = geodesic(answer_location, target_location).kilometers
tolerance_km = float(tolerance_radius) / 1000 # Convert meters to kilometers
print(f"Distance: {distance_km} km, Tolerance: {tolerance_km} km")
# Calculate base score with non-linear decrease
if distance_km <= tolerance_km:
# Direct hit - full points + bonus
base_score = 1000 + DIRECT_HIT_BONUS
is_correct = True
else:
if distance_km > MAX_DISTANCE_KM:
# Beyond max distance - 0 points
base_score = 0
is_correct = False
else:
# Non-linear decrease using exponential falloff
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
base_score = 1000 * (1 - falloff)
is_correct = True
# Apply time penalty (reduces score based on time taken)
if is_correct and base_score > 0:
# Calculate time taken as a fraction of total time (0-1)
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
max_time_sec = int(current_question.time_per_question)
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
else:
final_score = base_score # 0 points for incorrect answers
# Apply max score limit after all calculations (including bonus)
final_score = min(final_score, 1200) # Cap at 1200 (1000 + 200 bonus)
score = int(round(final_score))
except Exception as e:
print(f"Error calculating score: {e}")
is_correct = False
score = 0
# Save the answer
answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant,
question_index=quiz_game.current_question_index,
defaults={
'answer_index': answer_index,
'is_correct': is_correct,
'score': score,
'time_remaining': time_remaining
}
)
if not created:
answer_obj.answer_index = answer_index
answer_obj.is_correct = is_correct
answer_obj.score = score
answer_obj.time_remaining = time_remaining
answer_obj.save()
participant.last_score = score
participant.score += score
participant.coordinate_answer= answer_location_coords
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def save_answer(self, participant_id, answer, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
is_correct = False
answer_index = None
answers_order = None
answer_text = None
if question_type in ["multiple_choice", "true_false"]:
# answer ist hier eine Integer-Antwort (Index)
answer_index = int(answer)
if answer_index >= 0:
is_correct = question_data['options'][answer_index].get('is_correct', False)
elif question_type == "input":
user_input = str(answer).strip().lower().replace(" ", "")
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
is_correct = (user_input == correct_value)
answer_text = user_input
answer_index = 0 if is_correct else 1
elif question_type == "order":
# answer ist hier eine Liste von Strings mit der Reihenfolge
answers_order = answer
correct_sequence = sorted(question_data["options"], key=lambda x: x["order"])
correct_values = [opt["value"] for opt in correct_sequence]
is_correct = (answers_order == correct_values)
elif question_type == "guess":
try:
user_val = float(answer)
correct_val = float(question_data['options'][0].get('value', 0))
tolerance = float(question_data.get('tolerance', 0))
percentage = 1 - abs(user_val - correct_val) / (abs(correct_val) + 1e-5)
score_percentage= min(round(percentage, 3), 1.0)
if score_percentage>= abs(tolerance/100):
is_correct=True
else:
is_correct=False
print(score_percentage)
except ValueError:
is_correct = False # falls keine Zahl eingegeben wurde
answer_index = 0 if is_correct else 1
# Score berechnen
try:
if score_percentage==None:
score_percentage=1
except:
score_percentage=1
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / int(current_question.time_per_question) / 1000
score = int(base_score * (0.75 + 0.25 * time_factor))*score_percentage
print(score)
# Antwort speichern oder aktualisieren
answer_obj, created = QuizAnswer.objects.get_or_create(
participant=participant,
question_index=quiz_game.current_question_index,
defaults={
'answer_index': answer_index,
'answers_order': answers_order,
'is_correct': is_correct,
'score': score,
'time_remaining': time_remaining
}
)
if not created:
answer_obj.answer_index = answer_index
answer_obj.answers_order = answers_order
answer_obj.is_correct = is_correct
answer_obj.score = score
answer_obj.time_remaining = time_remaining
answer_obj.save()
# Teilnehmer aktualisieren
participant.last_score = score
participant.score += score
participant.last_answer=answer_index
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def get_answer_stats(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
answers = QuizAnswer.objects.filter(
participant__quiz_game=quiz_game,
question_index=quiz_game.current_question_index
)
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
question_type = question_data.get("type")
stats = {}
for single_answer in answers:
if question_type == "order":
correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])]
if single_answer.answers_order== correct_answer:
key = 0 # richtig
else:
key = 1 # falsch
stats[key] = stats.get(key, 0) + 1
else:
key = str(single_answer.answer_index if single_answer.answer_index is not None else "unanswered")
stats[key] = stats.get(key, 0) + 1
return stats
except:
pass
@database_sync_to_async
def save_rating(self, participant_id, rating):
from django.db import models
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
return False
from library.models import QuizRating
try:
game = QuizGame.objects.get(join_code=self.join_code)
participant = QuizGameParticipant.objects.get(
quiz_game=game,
participant_id=participant_id
)
# Update or create rating
"""
rating_obj, created = QuizRating.objects.get_or_create(
quiz=game.quiz_id,
participant_id=participant_id,
defaults={'rating': rating}
)
"""
rating_obj=QuizRating.objects.create(
quiz=game.quiz_id,
participant_id=participant_id,
rating=rating)
rating_obj.rating = rating
rating_obj.save()
return True
except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist):
return False
@database_sync_to_async
def cleanup_game(self):
try:
game = QuizGame.objects.get(join_code=self.join_code)
active_participants = QuizGameParticipant.objects.filter(
quiz_game=game
).count()
if active_participants == 0:
game.delete()
except QuizGame.DoesNotExist:
pass
@database_sync_to_async
def get_participants(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
return [
{
'id': p.participant_id,
'display_name': p.display_name,
'score': p.score
}
for p in participants
]
except QuizGame.DoesNotExist:
return []
@database_sync_to_async
def _advance_to_next_question(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz = quiz_game.quiz_id
# Move to next question
quiz_game.current_question_index += 1
# Check if we've reached the end
if quiz_game.current_question_index >= quiz.questions.count():
quiz_game.current_state = 'finished'
quiz_game.save()
return 'finished'
else:
# Update game state and start time
quiz_game.current_state = 'question'
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return 'question'
except QuizGame.DoesNotExist:
return None
async def advance_to_next_question(self):
result = await self._advance_to_next_question()
if result == 'finished':
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
elif result == 'question':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'next_question',
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
elif result == 'winner_podest':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
elif result == 'scoreboard':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scores'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scores(self):
success = await self._show_scores()
if success:
# Notify clients to redirect to scores page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scores',
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def counter_played_quizzes(self): # Zähler, wie oft ein Quiz gespielt worden ist.
quiz = QuizGame.objects.get(join_code=self.join_code).quiz_id
quiz_game = QuizGame.objects.get(join_code=self.join_code)
if quiz_game.single_player_or_multiple=="multiple":
quiz.quiz_played_Moderiermodus+=1
active_participants = QuizGameParticipant.objects.filter(
quiz_game=quiz_game
).count()
quiz.quiz_played_Moderiermodus_players+=active_participants
else:
quiz.quiz_played_Lernmodus+=1
quiz.save()
@database_sync_to_async
def _finish_game(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def finish_game(self):
success = await self._finish_game()
await self.counter_played_quizzes()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_winner_podest(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'winner_podest'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_winner_podest(self):
success = await self._show_winner_podest()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_winner_podest',
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scoreboard(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scoreboard'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scoreboard(self):
success = await self._show_scoreboard()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scoreboard',
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
}
)

View File

@@ -1,235 +0,0 @@
from channels.generic.websocket import AsyncWebsocketConsumer
import json
import asyncio
from ..models import QuizGame, QuizGameParticipant
from channels.db import database_sync_to_async
import redis
# Dictionary to track inactive check tasks and active connections per game
game_check_tasks = {}
game_connections = {}
class LobbyConsumer(AsyncWebsocketConsumer):
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.room_group_name = f'lobby_{self.join_code}'
# Get participant ID from session
self.participant_id = self.scope['session'].get('participant_id')
# Track connection
if self.join_code not in game_connections:
game_connections[self.join_code] = set()
game_connections[self.join_code].add(self.channel_name)
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Send initial participants list
await self.update_participants()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Remove from connection tracking
if self.join_code in game_connections:
game_connections[self.join_code].discard(self.channel_name)
if not game_connections[self.join_code]:
# Last connection closed
del game_connections[self.join_code]
if self.join_code in game_check_tasks:
# Cancel the inactive check task
game_check_tasks[self.join_code].cancel()
del game_check_tasks[self.join_code]
async def receive(self, text_data):
data = json.loads(text_data)
message_type = data.get('type')
print(f'Received message type: {message_type}')
if message_type == 'ping':
# Respond with pong to keep connection alive
await self.send(text_data=json.dumps({
'type': 'pong'
}))
return
elif message_type in ['leave_game', 'kick_player']:
participant_id = data.get('participant_id')
if participant_id:
try:
# Get participant info before deletion
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(participant_id=participant_id)
display_name = participant.display_name
# For kick_player, verify that the request comes from the host
if message_type == 'kick_player':
game = await database_sync_to_async(lambda: participant.quiz_game)()
# Get host_id from message
host_id = data.get('host_id')
print(f'Kick attempt - Game host_id: {game.host_id}, Message host_id: {host_id}')
if not host_id:
print('No host_id in message')
return
if str(game.host_id) != str(host_id):
print(f'Host ID mismatch: {game.host_id} != {host_id}')
return
# Also verify against cookie as backup
cookie_host_id = self.scope.get('cookies', {}).get('host_id')
if not cookie_host_id or str(cookie_host_id) != str(host_id):
print(f'Cookie host_id mismatch: {cookie_host_id} != {host_id}')
return
# Delete the participant
await database_sync_to_async(participant.delete)()
# Notify other players
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'player_left',
'player_name': display_name,
'was_kicked': message_type == 'kick_player',
'participant_id': participant_id
}
)
# Update participants list
await self.update_participants()
# If it's the kicked player's connection, send them home
if participant_id == self.participant_id:
await self.send(text_data=json.dumps({
'type': 'redirect',
'url': '/'
}))
except QuizGameParticipant.DoesNotExist:
pass
elif message_type == 'update_participants':
await self.update_participants()
elif message_type == 'start_game':
host_id = data.get('host_id')
if host_id:
success = await self.start_game(host_id)
if success:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/question'
}
)
async def get_participants_list(self):
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=self.join_code)
participants = await database_sync_to_async(lambda: list(
game.participants
.values('participant_id', 'display_name')
))()
# Zustand laden
try:
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
# Alte Teilnehmer aus Redis holen
data = r.get(f"last_participants_{self.room_group_name}")
prev_names = set(json.loads(data)) if data else set()
# Aktuelle Teilnehmer aus dem Argument extrahieren
current_names = set(p['display_name'] for p in participants)
# Neue Teilnehmer bestimmen
new_participants = current_names - prev_names
# Benachrichtigung für neue Teilnehmer versenden
for name in new_participants:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'player_joined',
'player_name': name
}
)
# Teilnehmerliste in Redis aktualisieren
r.set(f"last_participants_{self.room_group_name}", json.dumps(list(current_names)))
except Exception as e:
print(f"Error in send_join_notifications: {e}")
self.last_participants = participants
return participants
except QuizGame.DoesNotExist:
return []
async def update_participants(self):
participants = await self.get_participants_list()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participants_list_update',
'participants': participants
}
)
async def participants_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participants_list_update',
'participants': event['participants']
}))
@database_sync_to_async
def start_game(self, host_id):
try:
game = QuizGame.objects.get(join_code=self.join_code)
if str(game.host_id) == str(host_id):
from django.utils import timezone
game.current_state = 'question'
game.question_start_time = timezone.now()
game.save()
return True
except QuizGame.DoesNotExist:
pass
return False
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
async def player_joined(self, event):
await self.send(text_data=json.dumps({
'type': 'player_joined',
'player_name': event['player_name']
}))
async def player_left(self, event):
message = {
'type': 'player_left',
'player_name': event['player_name'],
'was_kicked': event.get('was_kicked', False)
}
if event.get('was_kicked') and event.get('participant_id'):
message['participant_id'] = event['participant_id']
await self.send(text_data=json.dumps(message))

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 10:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0003_quizgameparticipant_participant_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='quizgame',
name='host_user',
),
migrations.AddField(
model_name='quizgame',
name='host_id',
field=models.CharField(default=None, max_length=200, unique=True),
preserve_default=False,
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 12:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0004_remove_quizgame_host_user_quizgame_host_id'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_heartbeat',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0005_quizgameparticipant_last_heartbeat'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='current_question_index',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('finished', 'Beendet')], default='lobby', max_length=20),
),
migrations.AddField(
model_name='quizgame',
name='question_start_time',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 18:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0006_quizgame_current_question_index_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizgameparticipant',
name='quiz_game',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='play.quizgame'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 18:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0007_alter_quizgameparticipant_quiz_game'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_answer',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='quizgameparticipant',
name='last_answer_correct',
field=models.BooleanField(blank=True, null=True),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 20:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0008_quizgameparticipant_last_answer_and_more'),
]
operations = [
migrations.CreateModel(
name='QuizAnswer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_index', models.IntegerField()),
('answer_index', models.IntegerField()),
('is_correct', models.BooleanField()),
('score', models.IntegerField()),
('time_remaining', models.IntegerField()),
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='play.quizgameparticipant')),
],
options={
'unique_together': {('participant', 'question_index')},
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-06 17:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0009_quizanswer'),
]
operations = [
migrations.AlterField(
model_name='quizgameparticipant',
name='last_heartbeat',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-21 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0010_alter_quizgameparticipant_last_heartbeat'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-22 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0011_quizgame_updated_at'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='last_score',
field=models.IntegerField(default=0, verbose_name='Letzte_Punkte'),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.1.7 on 2025-07-18 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0012_quizgameparticipant_last_score'),
]
operations = [
migrations.AddField(
model_name='quizanswer',
name='answer_text',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='quizanswer',
name='answers_order',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='quizanswer',
name='answer_index',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-07-25 16:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0013_quizanswer_answer_text_quizanswer_answers_order_and_more'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet')], default='lobby', max_length=20),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-07-28 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0014_alter_quizgame_current_state'),
]
operations = [
migrations.AlterField(
model_name='quizgame',
name='current_state',
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet'), ('scoreboard', 'Scoreboard')], default='lobby', max_length=20),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-01 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0015_alter_quizgame_current_state'),
]
operations = [
migrations.AddField(
model_name='quizgameparticipant',
name='coordinate_answer',
field=models.CharField(blank=True, max_length=256, null=True),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0016_quizgameparticipant_coordinate_answer'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='quiz_played_Lernmodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
field=models.IntegerField(blank=True, default=0, null=True),
),
migrations.AddField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
field=models.IntegerField(blank=True, default=0, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0017_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 14:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('play', '0018_quizgame_single_player_or_multiple'),
]
operations = [
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Lernmodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus',
),
migrations.RemoveField(
model_name='quizgame',
name='quiz_played_Moderiermodus_players',
),
migrations.RemoveField(
model_name='quizgame',
name='single_player_or_multiple',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-14 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('play', '0019_remove_quizgame_quiz_played_lernmodus_and_more'),
]
operations = [
migrations.AddField(
model_name='quizgame',
name='single_player_or_multiple',
field=models.CharField(blank=True, max_length=20),
),
]

View File

@@ -6,23 +6,9 @@ import random, string
# Create your models here.
class QuizGame(models.Model):
GAME_STATES = [
('lobby', 'In Lobby'),
('question', 'Frage läuft'),
('scores', 'Punkteübersicht'),
('winner_podest','Siegerpodest'),
('finished', 'Beendet'),
('scoreboard', 'Scoreboard'),
]
single_player_or_multiple=models.CharField(max_length=20, blank=True)
host_id = models.CharField(max_length=200, unique=True)
host_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='host_user')
join_code = models.CharField(max_length=6, unique=True, blank=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE)
current_state = models.CharField(max_length=20, choices=GAME_STATES, default='lobby')
current_question_index = models.IntegerField(default=0)
question_start_time = models.DateTimeField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
if not self.join_code:
@@ -35,23 +21,12 @@ class QuizGame(models.Model):
if not QuizGame.objects.filter(join_code=new_code).exists():
return new_code
def generate_unique_id(self):
for i in range(10):
new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60))
if not QuizGameParticipant.objects.filter(participant_id=new_id).exists():
return new_id
class QuizGameParticipant(models.Model):
participant_id = models.CharField(max_length=200, unique=True)
display_name = models.CharField(verbose_name="Anzeigename", max_length=15)
quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants")
quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participant")
score = models.IntegerField(verbose_name="Punkte", default=0)
last_score = models.IntegerField(verbose_name="Letzte_Punkte", default=0)
avatar = models.CharField(max_length=200, blank=True, default="")
last_heartbeat = models.DateTimeField(auto_now_add=True)
last_answer = models.IntegerField(null=True, blank=True)
coordinate_answer = models.CharField(null=True, blank=True,max_length=256)
last_answer_correct = models.BooleanField(null=True, blank=True)
def save(self, *args, **kwargs):
if not self.participant_id:
@@ -63,22 +38,3 @@ class QuizGameParticipant(models.Model):
new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60))
if not QuizGameParticipant.objects.filter(participant_id=new_id).exists():
return new_id
class QuizAnswer(models.Model):
participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers')
question_index = models.IntegerField()
answer_index = models.IntegerField(null=True, blank=True) # für MC, TF
answer_text = models.TextField(null=True, blank=True) # für Input
answers_order = models.JSONField(null=True, blank=True) # NEU für "order"
is_correct = models.BooleanField()
score = models.IntegerField()
time_remaining = models.IntegerField()
class Meta:
unique_together = ['participant', 'question_index']
def generate_unique_id(self):
for i in range(10):
new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60))
if not QuizGameParticipant.objects.filter(participant_id=new_id).exists():
return new_id

View File

@@ -1,8 +0,0 @@
from django.urls import re_path
from .consumers.game import GameConsumer
from .consumers.lobby import LobbyConsumer
websocket_urlpatterns = [
re_path(r'ws/game/(?P<join_code>\w+)/$', GameConsumer.as_asgi()),
re_path(r'ws/play/lobby/(?P<join_code>\w+)/$', LobbyConsumer.as_asgi()),
]

View File

@@ -4,16 +4,9 @@ from . import views
app_name = 'play'
urlpatterns = [
path('game/new/<int:quiz_id>', views.create_game, name='create_game'),
path('game/<str:join_code>', views.lobby, name='lobby'),
path('game/<str:join_code>/participate', views.create_participant, name='create_participant'),
path('join', views.join_game, name='join_game'),
path('game/<str:join_code>/question', views.question, name='question'),
path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'),
path('game/<str:join_code>/scores', views.scores, name='scores'),
path('game/<str:join_code>/finished', views.finished, name='finished'),
path('game/<str:join_code>/winner_podest', views.winner_podest, name='winner_podest'),
path('game/<str:join_code>/scoreboard', views.scoreboard, name='scoreboard'),
path('select_mode/<str:join_code>', views.select_mode, name='select_mode'),
path('selected_mode/<str:join_code>', views.selected_mode, name='selected_mode'),
path('game/<str:join_code>/<int:question_id>', views.question, name='question'),
path('game/<str:join_code>/<int:question_id>/participant', views.question_participant, name='question_participant'),
]

View File

@@ -1,483 +1,86 @@
from django.utils import timezone
from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404, reverse
from play.models import QuizGame, QuizGameParticipant
from library.models import QivipQuiz
from django.http import HttpResponseRedirect, HttpResponseNotFound
from django.contrib import messages
from django.conf import settings
from library.models import QivipQuiz, QivipQuestion
from django.http import HttpResponseRedirect
import json
import qrcode
import base64
from io import BytesIO
from django.shortcuts import render
# Create your views here.
def lobby(request, join_code):
relative_url = reverse("play:create_participant", kwargs={"join_code": join_code})
full_url = request.build_absolute_uri(relative_url)
qr = qrcode.make(full_url)
buffer = BytesIO()
qr.save(buffer, format="PNG")
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
context = {
'quiz': quiz,
'quiz_game': quiz_game,
"qr_code_base64": img_base64,
}
mode = request.GET.get('mode','default')
request.session['mode'] = mode
if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id:
context['host_id'] = host_id
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context)
elif mode=="learn":
return redirect('play:create_participant', join_code=join_code)
if not "participant_id" in request.COOKIES:
return redirect('play:create_participant', join_code=join_code)
participant_id = request.COOKIES['participant_id']
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen
participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save()
except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code)
context['participant'] = participant
context["qr_code_base64"] = img_base64
return render(request, 'play/lobby.html', context=context)
def select_mode(request,join_code):
return render(request, 'play/select_mode.html', {'join_code': join_code})
def create_participant(request, join_code):
mode = request.GET.get('mode','default')
request.session['mode'] = mode
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if mode!="learn":
if "participant_id" in request.COOKIES:
# Prüfe ob der Teilnehmer noch existiert
participant_id = request.COOKIES['participant_id']
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
if participant.quiz_game.join_code != join_code:
participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game
participant.last_score = 0
participant.save()
return redirect('play:lobby', join_code=join_code)
except QuizGameParticipant.DoesNotExist:
# Cookie löschen wenn Teilnehmer nicht mehr existiert
response = HttpResponseRedirect(request.path)
response.delete_cookie('participant_id')
return response
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
participant = get_object_or_404(QuizGameParticipant, participant_id=participant_id)
if request.method == 'POST':
display_name = request.POST.get('display_name')
if not display_name:
return render(request, 'play/initialize_participant.html', {
'join_code': join_code,
'error': 'Bitte geben Sie einen Namen ein'
})
participant = QuizGameParticipant()
participant.display_name = display_name
if not participant.quiz_game.join_code == join_code:
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.last_score = 0
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
response.set_cookie('participant_id', participant.participant_id, max_age=3600)
return response
context = {
'debug': "test",
'participant': participant,
'quiz': quiz,
}
return render(request, 'play/initialize_participant.html', {'join_code': join_code})
else:
messages.error(request, "Beitritt nicht möglich.")
return render(request, 'play/join_game.html')
def create_game(request, quiz_id):
return render(request, 'play/lobby.html', context=context)
try:
quiz = QivipQuiz.objects.get(id=quiz_id)
game = QuizGame()
game.quiz_id = quiz
game.host_id = game.generate_unique_id()
game.save()
def create_participant(request, join_code):
if "participant_id" in request.COOKIES: # Umleiten, falls Teilnehmer bereits erstellt
return redirect('play:lobby', join_code=join_code)
if request.method == 'POST':
display_name = request.POST.get('display_name')
join_code = request.POST.get('join_code')
try:
quiz = QuizGame.objects.get(join_code=join_code)
participant = QuizGameParticipant()
participant.display_name = display_name
participant.quiz_game = quiz
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
response.set_cookie('participant_id', participant.participant_id, max_age=3600)
response = HttpResponseRedirect(reverse('play:select_mode', kwargs={'join_code': game.join_code}))
response.set_cookie('host_id', game.host_id, max_age=3600)
return response
except QivipQuiz.DoesNotExist:
return HttpResponseNotFound("Quiz nicht gefunden")
return response
except QuizGame.DoesNotExist:
# TODO: Fehlermeldung fuer nicht-existierendes Quiz
pass
return render(request, 'play/initialize_participant.html', {'join_code': join_code})
def join_game(request):
if request.method == 'POST':
join_code = request.POST.get('game_code')
try:
quiz = QuizGame.objects.get(join_code=join_code)
# Redirect to create_participant if no participant cookie exists
if not "participant_id" in request.COOKIES:
return redirect('play:create_participant', join_code=join_code)
return redirect('play:lobby', join_code=join_code)
except QuizGame.DoesNotExist:
messages.error(request, "Dieses Spiel existiert nicht.")
# TODO: Mit message eine Fehlermeldung weitergeben (und im entsprechenden Template Designen)
pass
return render(request, 'play/join_game.html')
def question(request, join_code, question_id):
question = get_object_or_404(QivipQuestion, pk=question_id)
def scores(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'scores':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/score_overview.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
def scoreboard(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'scoreboard':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/scoreboard.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
"""
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'finished':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get participant if exists
participant_id = request.COOKIES.get('participant_id')
participant = None
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
participant_id=participant_id
).first()
return render(request, 'play/game/finished.html', {
'quiz_game': quiz_game,
'participants': participants,
'winner': participants.first() if participants.exists() else None,
'join_code': join_code,
'is_host': is_host,
'participant_id': participant_id if participant else None
})
"""
def winner_podest(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'winner_podest':
return redirect('play:lobby', join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Check if user is host
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
# Get current question for context
questions = quiz_game.quiz_id.questions.all()
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
try:
my_participant = request.session.get('my_participant-participant_id')
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
my_participant=participant
except:
participant = QuizGameParticipant.objects.none()
return render(request, 'play/game/winner_podest.html', {
'quiz_game': quiz_game,
'participants': participants,
'participant': participant,
'my_participant':my_participant,
'is_host': is_host,
'host_id': quiz_game.host_id if is_host else None,
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
'question_data': question_data,
'question_index': quiz_game.current_question_index,
'total_questions': len(questions)
})
def finished(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
if quiz_game.current_state != 'finished':
return redirect('play:lobby', join_code=join_code)
try:
game = QuizGame.objects.get(join_code=join_code)
# Get participants sorted by score
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
# Get participant if exists
participant_id = request.COOKIES.get('participant_id')
participant = None
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
participant_id=participant_id
).first()
is_host = False
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
is_host = True
context = {
'join_code': join_code,
'is_host': is_host,
'participants': participants,
'participant_id': participant_id if participant else None,
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
def question(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Verify game state
if quiz_game.current_state != 'question':
return redirect('play:lobby', join_code=join_code)
# Get current question
questions = quiz_game.quiz_id.questions.all()
if quiz_game.current_question_index >= len(questions):
return redirect('play:lobby', join_code=join_code)
current_question = questions[quiz_game.current_question_index]
question_data = json.loads(current_question.data)
# Check if user is host
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
return render(request, 'play/game/question_host.html', {
'quiz_game': quiz_game,
'question_data': question_data,
'current_question':current_question,
'host_id': quiz_game.host_id,
'start_time': int(quiz_game.question_start_time.timestamp() * 1000),
'question_index': quiz_game.current_question_index,
'total_questions': len(questions),
'question_image': current_question.image
})
# Handle participant view
if not 'participant_id' in request.COOKIES:
return redirect('play:create_participant', join_code=join_code)
participant_id = request.COOKIES['participant_id']
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code)
request.session['my_participant-participant_id'] = participant.participant_id
return render(request, 'play/game/question_participant.html', {
'quiz_game': quiz_game,
'question_data': question_data,
'current_question':current_question,
'participant': participant,
'start_time': int(quiz_game.question_start_time.timestamp() * 1000),
'question_index': quiz_game.current_question_index,
'total_questions': len(questions),
})
def waiting_room(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
# Check if user is host
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
return redirect('play:question', join_code=join_code)
# Handle participant view
if not 'participant_id' in request.COOKIES:
return redirect('play:create_participant', join_code=join_code)
participant_id = request.COOKIES['participant_id']
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
except QuizGameParticipant.DoesNotExist:
return redirect('play:create_participant', join_code=join_code)
return render(request, 'play/game/wait_for_other_players.html', {
'quiz_game': quiz_game,
'participant': participant
})
def selected_mode(request, join_code):
mode = request.GET.get('mode','default')
request.session['mode'] = mode
quiz_game = QuizGame.objects.get(join_code=join_code)
if mode=="learn":
quiz_game.single_player_or_multiple="single"
else:
quiz_game.single_player_or_multiple="multiple"
quiz_game.save()
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete()
# Hier werden die Fragen gemischt!
import random
questions = quiz_game.quiz_id.questions.all()
for question in questions:
try:
data = json.loads(question.data)
if (data.get('type') == 'order' or data.get('type') == 'multiple_choice') and 'options' in data:
random.shuffle(data['options'])
question.data = json.dumps(data)
question.save(update_fields=['data'])
except Exception:
pass
if question.data:
question.data = json.loads(question.data)
context = {
'quiz': quiz,
'quiz_game': quiz_game,
'question': question,
}
if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id:
context['host_id'] = host_id
if mode=="learn":
participant = QuizGameParticipant()
if request.user.is_authenticated:
participant.display_name = request.user.username
else:
participant.display_name = "SIE"
return render(request, 'play/game/question_host.html', {'question': question, 'debug': "debug"})
participant.participant_id=host_id
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.last_score = 0
participant.save()
quiz_game.current_state = "question"
quiz_game.question_start_time = timezone.now()
quiz_game.save()
def question_participant(request, join_code, question_id):
question = get_object_or_404(QivipQuestion, pk=question_id)
return redirect('play:question',join_code)
if question.data:
question.data = json.loads(question.data)
return redirect('play:lobby', join_code=join_code)
context = {
'question': question,
}
return render(request, 'play/game/question_participant.html', {'question': question, 'debug': "debug"})

View File

@@ -3,9 +3,6 @@
qp-container {
@apply max-w-xl w-full mx-auto;
}
.dark-form label {
@apply dark:text-white!;
}
nav div ul.qp-nav-list li {
@apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200;
@@ -56,44 +53,11 @@ a.qp-a-button-small {
}
.field-group form p {
@apply flex flex-col gap-2;
}
.field-group form p label {
@apply text-gray-900 font-semibold text-sm;
}
.field-group form p input {
@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p select {
@apply p-3 rounded-md bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
.field-group form p textarea {
@apply p-3 rounded-xl bg-gray-200 focus:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 min-h-[100px];
}
.field-group form p button {
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105
transition duration-200 font-black shadow-md hover:shadow-lg;
}
ul.messages li {
@apply p-3 rounded-full bg-gray-200 hover:scale-105 transition duration-200 font-black
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
}
ul.messages li.error {
@apply bg-red-200;
}
@@ -137,15 +101,6 @@ div.qp-answer-container-host div p {
@apply font-black text-xl p-4;
}
/* Filter Panel Styles */
#filterPanel {
@apply hidden transition-all duration-300 ease-in-out;
}
#filterPanel.show {
@apply block;
}
div.qp-question-container {
@apply text-center text-3xl px-4 py-9 h-screen;
}
@@ -153,12 +108,3 @@ div.qp-question-container {
div.qp-question-container img {
@apply max-w-[50vw] max-h-[40vh] mx-auto;
}
div#mapquestion-container {
display: flex;
justify-content: center;
}
div#map {
@apply md:w-[600px] lg:w-[900px] h-[400px]
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More