Compare commits

..

1 Commits

Author SHA1 Message Date
nik8
69dc2530ce Schrift in Kasten 2025-03-16 17:48:31 +01:00
62 changed files with 228 additions and 4219 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,3 @@ db.sqlite3
tailwindcss.exe tailwindcss.exe
t-style.css t-style.css
dump.rdb dump.rdb
media
node_modules
staticfiles

View File

@@ -47,42 +47,16 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
### Entwicklungsserver starten ### 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: - Standardport (8000):
```sh ```sh
source .venv/bin/activate # Linux python3 core/manage.py runserver
.venv\Scripts\activate # Windows ```
``` - Bestimmten Port (z. B. 9000):
```sh
2. Starte den Daphne-Server: python3 core/manage.py runserver 9000
```sh ```
# Im Verzeichnis 'django'
daphne -b 127.0.0.1 -p 8000 core.asgi:application
```
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)
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden! Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
@@ -126,10 +100,10 @@ npx @tailwindcss/cli -i <INPUT-DATEI> -o <OUTPUT-DATEI> --watch --minify
```sh ```sh
# unter Linux mit Tailwind Binärdatei: # unter Linux mit Tailwind Binärdatei:
npx tailwindcss -i static/css/t-input.css -o static/css/t-style.css --watch --minify tailwindcss -i core/static/homepage/t-input.css -o core/static/homepage/t-style.css --watch --minify
# unter Windows mit NPM (in bash mit '/' statt '\'): # unter Windows mit NPM:
npx @tailwindcss/cli -i ./static/css/t-input.css -o ./static/css/t-style.css --watch --minify npx @tailwindcss/cli -i .\core\static\homepage\t-input.css -o .\core\static\homepage\t-style.css --watch --minify
``` ```
## Issue - Merge Request - Merge ## Issue - Merge Request - Merge

View File

@@ -1,6 +1,5 @@
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from .forms import RegisterForm from .forms import RegisterForm
# Create your views here. # Create your views here.
@@ -12,9 +11,8 @@ def register(response):
if response.method == "POST": if response.method == "POST":
form = RegisterForm(response.POST) form = RegisterForm(response.POST)
if form.is_valid(): if form.is_valid():
user=form.save() form.save()
login(response, user) #automatischer Login nach Registrierung return redirect("home")
return redirect("accounts:home")
else: else:
form = RegisterForm() form = RegisterForm()

View File

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

View File

@@ -42,12 +42,10 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@@ -75,13 +73,6 @@ TEMPLATES = [
] ]
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
# Database # Database
@@ -94,15 +85,6 @@ DATABASES = {
} }
} }
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -139,22 +121,11 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/account/login/' LOGIN_URL = '/account/login/'
import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien

View File

@@ -14,8 +14,6 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 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.contrib import admin # type: ignore
from django.urls import path, include from django.urls import path, include
@@ -25,8 +23,4 @@ urlpatterns = [
path('', include('homepage.urls')), path('', include('homepage.urls')),
path('play/', include('play.urls')), path('play/', include('play.urls')),
path('library/', include('library.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,14 +1,15 @@
from django.contrib import admin from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QuizCategory from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag
# Für das QivipQuiz Modell # Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin): class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name', '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 search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen list_filter = ('status', 'category') # Filteroptionen
# Für das QivipQuestion Modell # Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin): class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date') list_display = ('quiz_id', 'data', 'creation_date', 'update_date')
search_fields = ('data',) search_fields = ('data',)
list_filter = ('quiz_id',) list_filter = ('quiz_id',)
@@ -27,4 +28,4 @@ class TagAdmin(admin.ModelAdmin):
admin.site.register(QivipQuiz, QivipQuizAdmin) admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin) admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCategory, QuizCategoryAdmin) admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(Tag, TagAdmin)

View File

@@ -4,28 +4,9 @@ from .models import QivipQuiz, QivipQuestion
class QuizForm(forms.ModelForm): class QuizForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description','image', 'status', 'category', "difficulty","credits"] fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"]
class QuestionForm(forms.ModelForm): class QuestionForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuestion model = QivipQuestion
fields = ['quiz_id', 'data'] 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 ...',
}))
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_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'
}))

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-21 17:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0022_alter_qivipquiz_description_alter_qivipquiz_name'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='quiz_images/'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-22 10:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0023_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/'),
),
]

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,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-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

@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
class QivipQuiz(models.Model): class QivipQuiz(models.Model):
STATUS_VALUES = { STATUS_VALUES = {
"öffentlich": "Öffentlich", "öffentlich": "Öffentlich",
#"versteckt": "Versteckt", "versteckt": "Versteckt",
"privat": "Privat", "privat": "Privat",
} }
DIFFICULTY_VALUES = { DIFFICULTY_VALUES = {
@@ -24,31 +24,26 @@ class QivipQuiz(models.Model):
if value.strip() == "In dem Quiz geht es um ...": if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.") raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
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) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') 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) creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich") status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich")
category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE) category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
name = models.CharField(max_length=75) name = models.CharField(max_length=75)
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...") 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") difficulty= models.CharField(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)
def __str__(self): def __str__(self):
return self.name return self.name
# Create your models here. # Create your models here.
class QivipQuestion(models.Model): class QivipQuestion(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 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) creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
data = models.TextField() data = models.TextField()
@@ -56,29 +51,11 @@ class QivipQuestion(models.Model):
def __str__(self): def __str__(self):
return self.data[:50] return self.data[:50]
class Tag(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
class QuizRating(models.Model): return self.name
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):
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()
class QuizCategory(models.Model): class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)

View File

@@ -1,8 +1,6 @@
from django.urls import path from django.urls import path
from django.conf import settings
from . import views from . import views
from django.conf.urls.static import static
app_name = 'library' app_name = 'library'
urlpatterns = [ urlpatterns = [
@@ -14,6 +12,4 @@ urlpatterns = [
path('question/new/', views.new_question, name='new_question'), path('question/new/', views.new_question, name='new_question'),
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'), path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'), path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
] ]

View File

@@ -1,4 +1,3 @@
import uuid
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -8,103 +7,29 @@ from .models import QivipQuiz, QivipQuestion
from .forms import QuizForm, QuestionForm from .forms import QuizForm, QuestionForm
import json import json
from django.core.paginator import Paginator from django.core.paginator import Paginator
from .models import QivipQuiz
from .forms import QuizFilterForm
from django.db.models import Count
from django.contrib.auth.models import User
# Übersicht aller Quizze # Übersicht aller Quizze
def overview_quiz(request): def overview_quiz(request):
# Filter try:
form = QuizFilterForm(request.GET) quizzes = QivipQuiz.objects.filter(user_id=request.user)
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').exclude(user_id=request.user).filter(question__isnull=False).distinct()
# Initialize querysets return render(request, 'library/overview_quiz.html', {'quizzes': quizzes, 'all_quizzes': all_quizzes})
if request.user.is_authenticated:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
else:
filter_my_quizzes = QivipQuiz.objects.none()
filter_other_quizzes = QivipQuiz.objects.all()
# Apply common filters to other quizzes
filter_other_quizzes = filter_other_quizzes.annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1, status='öffentlich')
if form.is_valid():
search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amout_questions= form.cleaned_data.get('max_amout_questions')
min_amout_questions= form.cleaned_data.get('min_amout_questions')
if search: # Suche nach Namen
filter_other_quizzes = filter_other_quizzes.filter(name__icontains=search)
filter_my_quizzes = filter_my_quizzes.filter(name__icontains=search)
if min_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=min_amout_questions)
filter_my_quizzes = filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions) if filter_my_quizzes else QivipQuiz.objects.none()
if max_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__lte=max_amout_questions)
try:
filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__lte=max_amout_questions)
except:
filter_my_quizzes = QivipQuiz.objects.none()
if username:
try:
user = User.objects.get(username=username) # Benutzer anhand des Namens holen
filter_other_quizzes = filter_other_quizzes.filter(user_id=user.id)
filter_my_quizzes = filter_my_quizzes.filter(user_id=user.id) # Nach der user_id filtern
except User.DoesNotExist:
filter_other_quizzes = QivipQuiz.objects.none() # Falls User nicht existiert, leere Query zurückgeben
filter_my_quizzes = QivipQuiz.objects.none()
try:
filter_other_quizzes=filter_other_quizzes.exclude(user_id=request.user)
except:
filter_other_quizzes=filter_other_quizzes
# Pagination for my quizzes
my_quizzes_paginator = Paginator(filter_my_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
my_quizzes = my_quizzes_paginator.page(request.GET.get('my_page', 1))
except:
my_quizzes = my_quizzes_paginator.page(1)
# Pagination for other quizzes
other_quizzes_paginator = Paginator(filter_other_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
other_quizzes = other_quizzes_paginator.page(request.GET.get('other_page', 1))
except:
other_quizzes = other_quizzes_paginator.page(1)
context = {
'show_search': True,
'my_quizzes': my_quizzes,
'other_quizzes': other_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
except:
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct()
return render(request, 'library/overview_quiz.html', {'quizzes': None, 'all_quizzes': all_quizzes})
# Neues Quiz erstellen # Neues Quiz erstellen
@login_required @login_required
def new_quiz(request): def new_quiz(request):
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, request.FILES) form = QuizForm(request.POST)
if form.is_valid(): if form.is_valid():
quiz = form.save(commit=False) quiz = form.save(commit=False)
quiz.user_id = request.user quiz.user_id = request.user
#quiz.creator = request.user
quiz.save() quiz.save()
form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags) form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:edit_quiz', pk=quiz.pk)
else: else:
form = QuizForm() form = QuizForm()
return render(request, 'library/form.html', {'form': form}) return render(request, 'library/form.html', {'form': form})
@@ -114,13 +39,10 @@ def new_quiz(request):
def edit_quiz(request, pk): def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz, files=request.FILES) form = QuizForm(request.POST, instance=quiz)
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect('library:detail_quiz', pk=pk) return redirect('library:overview_quiz')
#return modified(request, pk)
else: else:
form = QuizForm(instance=quiz) form = QuizForm(instance=quiz)
return render(request, 'library/form.html', {'form': form}) return render(request, 'library/form.html', {'form': form})
@@ -135,11 +57,11 @@ def delete_quiz(request, pk):
return render(request, 'library/delete_confirmation.html', {'object': quiz}) return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen # Quiz anzeigen
@login_required
def detail_quiz(request, pk): def detail_quiz(request, pk):
#quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz) questions = QivipQuestion.objects.filter(quiz_id=quiz)
# Parse JSON data for each question # Parse JSON data for each question
for question in questions: for question in questions:
if question.data: if question.data:
@@ -147,68 +69,9 @@ def detail_quiz(request, pk):
context = { context = {
'quiz': quiz, 'quiz': quiz,
'questions': questions, 'questions': questions
'detail':show_answers,
} }
return render(request, 'library/detail_quiz.html', context) return render(request, 'library/detail_quiz.html', context)
"""
def modified(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
words=original_quiz.modified_description.split()
word_exist=0
print(original_quiz.creator.username)
print(request.user.username)
for word in words:
if word==request.user.username or word+","==request.user.username:
word_exist=1
if word_exist==0 and len(words)!=0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=", "+ request.user.username
elif word_exist==0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=" "+ request.user.username
original_quiz.save()
return redirect('library:detail_quiz', pk=pk)
"""
@login_required
def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
new_quiz = original_quiz # Kopie erstellen (aber Achtung: Noch gleiche Referenz!)
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
new_quiz.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie" # Optional: Name anpassen
new_quiz.base_quiz_id = pk
#new_quiz.creator = original_quiz.creator or request.user
new_quiz.save() # Speichern als neues Objekt
# Optional: Beschreibung anpassen
# Alle zugehörigen Fragen kopieren
questions = QivipQuestion.objects.filter(quiz_id=pk)
for question in questions:
question.uuid = uuid.uuid4()
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
question.quiz_id = original_quiz # Verknüpfe mit dem neuen Quiz
question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!")
return redirect('library:detail_quiz', pk=new_quiz.pk)
# Übersicht aller Fragen # Übersicht aller Fragen
@login_required @login_required
@@ -284,7 +147,6 @@ def new_question(request):
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.quiz_id = quiz question.quiz_id = quiz
question.save() question.save()
#return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:detail_quiz', pk=quiz.pk)
else: else:
# Initialize empty question data for new questions # Initialize empty question data for new questions
@@ -413,9 +275,7 @@ def edit_question(request, pk):
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.save() question.save()
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk) return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else: else:
template_name = f'library/question/question_{question_type}.html' template_name = f'library/question/question_{question_type}.html'
context = { context = {
@@ -426,7 +286,6 @@ def edit_question(request, pk):
return render(request, template_name, context) return render(request, template_name, context)
# Frage löschen # Frage löschen
@login_required @login_required
def delete_question(request, pk): def delete_question(request, pk):

View File

@@ -1,35 +0,0 @@
{
"name": "django",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "django",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"swiper": "^11.2.6"
}
},
"node_modules/swiper": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz",
"integrity": "sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
}
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "django",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"swiper": "^11.2.6"
}
}

View File

@@ -1,527 +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.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.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,492 +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 django.db import models
from datetime import timedelta
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
# Dictionary to track inactive check tasks per game
game_check_tasks = {}
class GameConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
game_group_name = f'game_{join_code}'
while True:
try:
await asyncio.sleep(30) # Check every 30 seconds
try:
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(minutes=1)
# Get active and inactive participants
all_participants = await database_sync_to_async(lambda: list(
game.participants.all().values('participant_id', 'display_name', 'last_heartbeat')
))()
# Konvertiere datetime zu ISO Format String
for p in all_participants:
if p['last_heartbeat']:
p['last_heartbeat'] = p['last_heartbeat'].isoformat()
active_participants = [p for p in all_participants if p['last_heartbeat'] and timezone.datetime.fromisoformat(p['last_heartbeat']) >= cutoff_time]
inactive_participants = [p for p in all_participants if not p['last_heartbeat'] or timezone.datetime.fromisoformat(p['last_heartbeat']) < cutoff_time]
# Remove inactive participants
if inactive_participants:
for p in inactive_participants:
# Benachrichtige andere über den gekickten Spieler
await channel_layer.group_send(
game_group_name,
{
'type': 'player_left',
'player_name': p['display_name'],
'was_kicked': True
}
)
# Lösche den inaktiven Teilnehmer
await database_sync_to_async(QuizGameParticipant.objects.filter(
participant_id=p['participant_id']
).delete)()
# Broadcast update to all clients
if active_participants or inactive_participants:
await channel_layer.group_send(
game_group_name,
{
'type': 'participant_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players in game: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in game inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
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()
# Ensure inactive check is running for this game
await self.start_inactive_check(self.join_code, self.channel_layer)
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()
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 == 'heartbeat':
participant_id = text_data_json.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Update participants list after heartbeat
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
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)
# 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 == '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 == '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')
}))
async def update_participant_heartbeat(self, participant_id):
"""Update the last heartbeat timestamp for a participant."""
try:
participant = await database_sync_to_async(QuizGameParticipant.objects.get)(
participant_id=participant_id
)
participant.last_heartbeat = timezone.now()
await database_sync_to_async(participant.save)()
except QuizGameParticipant.DoesNotExist:
pass
@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
question_data = json.loads(current_question.data)
is_correct = 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))
# Update or create answer in QuizAnswer table
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.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)
# Count answers for each option in the current question
stats = {}
answers = QuizAnswer.objects.filter(
participant__quiz_game=quiz_game,
question_index=quiz_game.current_question_index
)
for answer in answers:
if answer.answer_index >= 0: # Ignore timeouts (-1)
stats[answer.answer_index] = stats.get(answer.answer_index, 0) + 1
return stats
except QuizGame.DoesNotExist:
return {}
@database_sync_to_async
def save_rating(self, participant_id, rating):
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}
)
if not created:
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)
# Get all active participants (heartbeat within last minute)
from django.utils import timezone
active_participants = QuizGameParticipant.objects.filter(
quiz_game=game,
last_heartbeat__gte=timezone.now() - timezone.timedelta(minutes=1)
).count()
# If no active participants and game is finished for more than 5 minutes, delete it
if active_participants == 0 and game.current_state == 'finished':
if game.question_start_time and (timezone.now() - game.question_start_time).total_seconds() > 300:
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})
}
)
@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})
}
)

View File

@@ -1,282 +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
# Dictionary to track inactive check tasks and active connections per game
game_check_tasks = {}
game_connections = {}
class LobbyConsumer(AsyncWebsocketConsumer):
@classmethod
async def start_inactive_check(cls, join_code, channel_layer):
"""Start the inactive player check for a game if not already running."""
if join_code not in game_check_tasks or game_check_tasks[join_code].done():
async def check_inactive_players():
room_group_name = f'lobby_{join_code}'
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds to match frontend heartbeat
# Get participants list and broadcast update
try:
from django.utils import timezone
from datetime import timedelta
game = await database_sync_to_async(QuizGame.objects.get)(join_code=join_code)
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
# Get current active participants with full info
active_participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time)
.values('participant_id', 'display_name')
))()
# Send update to all clients
await channel_layer.group_send(
room_group_name,
{
'type': 'participants_list_update',
'participants': active_participants
}
)
except QuizGame.DoesNotExist:
break # Stop checking if game no longer exists
except Exception as e:
print(f'Error checking inactive players: {e}')
await asyncio.sleep(5) # Wait before retry
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in inactive check loop: {e}')
await asyncio.sleep(5)
game_check_tasks[join_code] = asyncio.create_task(check_inactive_players())
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)
# Start inactive check task if not already running
await self.start_inactive_check(self.join_code, self.channel_layer)
# 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
if message_type == 'heartbeat':
participant_id = data.get('participant_id')
if participant_id:
await self.update_participant_heartbeat(participant_id)
# Don't update participants list after heartbeat - let the background task handle it
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'
}
)
@database_sync_to_async
def update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
from django.utils import timezone
participant.last_heartbeat = timezone.now()
participant.save()
return True
except QuizGameParticipant.DoesNotExist:
return False
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)
# Only return participants that have been seen in the last minute
cutoff_time = timezone.now() - timedelta(seconds=20) # Consider inactive after 20 seconds (2 missed heartbeats)
participants = await database_sync_to_async(lambda: list(
game.participants.filter(last_heartbeat__gte=cutoff_time)
.values('participant_id', 'display_name')
))()
# Send join notification for new participants
try:
prev_names = set(p['display_name'] for p in self.last_participants) if hasattr(self, 'last_participants') else set()
current_names = set(p['display_name'] for p in participants)
new_participants = current_names - prev_names
for name in new_participants:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'player_joined',
'player_name': name
}
)
except Exception as e:
print(f'Error sending 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

@@ -6,19 +6,9 @@ import random, string
# Create your models here. # Create your models here.
class QuizGame(models.Model): class QuizGame(models.Model):
GAME_STATES = [ host_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='host_user')
('lobby', 'In Lobby'),
('question', 'Frage läuft'),
('scores', 'Punkteübersicht'),
('finished', 'Beendet')
]
host_id = models.CharField(max_length=200, unique=True)
join_code = models.CharField(max_length=6, unique=True, blank=True) join_code = models.CharField(max_length=6, unique=True, blank=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE) 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)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.join_code: if not self.join_code:
@@ -31,21 +21,12 @@ class QuizGame(models.Model):
if not QuizGame.objects.filter(join_code=new_code).exists(): if not QuizGame.objects.filter(join_code=new_code).exists():
return new_code 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): class QuizGameParticipant(models.Model):
participant_id = models.CharField(max_length=200, unique=True) participant_id = models.CharField(max_length=200, unique=True)
display_name = models.CharField(verbose_name="Anzeigename", max_length=15) 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) score = models.IntegerField(verbose_name="Punkte", default=0)
avatar = models.CharField(max_length=200, blank=True, default="") 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)
last_answer_correct = models.BooleanField(null=True, blank=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.participant_id: if not self.participant_id:
@@ -57,20 +38,3 @@ class QuizGameParticipant(models.Model):
new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60)) new_id = ''.join(random.choices(string.digits + string.ascii_lowercase, k=60))
if not QuizGameParticipant.objects.filter(participant_id=new_id).exists(): if not QuizGameParticipant.objects.filter(participant_id=new_id).exists():
return new_id 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()
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,12 +4,7 @@ from . import views
app_name = 'play' app_name = 'play'
urlpatterns = [ urlpatterns = [
path('game/new/<int:quiz_id>', views.create_game, name='create_game'), path('lobby/<str:join_code>', views.lobby, name='lobby'),
path('game/<str:join_code>', views.lobby, name='lobby'), path('lobby/<str:join_code>/participate', views.create_participant, name='create_participant'),
path('game/<str:join_code>/participate', views.create_participant, name='create_participant'),
path('join', views.join_game, name='join_game'), 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'),
] ]

View File

@@ -1,258 +1,60 @@
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import render, redirect, get_object_or_404, reverse from django.shortcuts import render, redirect, get_object_or_404, reverse
from play.models import QuizGame, QuizGameParticipant from play.models import QuizGame, QuizGameParticipant
from library.models import QivipQuiz, QivipQuestion from library.models import QivipQuiz
from django.http import HttpResponseRedirect, HttpResponseNotFound from django.http import HttpResponseRedirect
from django.contrib import messages
import json
# Create your views here. # Create your views here.
def lobby(request, join_code): def lobby(request, join_code):
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,
}
if "host_id" in request.COOKIES:
host_id = request.COOKIES['host_id']
if host_id == quiz_game.host_id:
context['host_id'] = host_id
return render(request, 'play/lobby.html', context=context)
if not "participant_id" in request.COOKIES: if not "participant_id" in request.COOKIES:
return redirect('play:create_participant', join_code=join_code) return redirect('play:create_participant', join_code=join_code)
participant_id = request.COOKIES['participant_id'] participant_id = request.COOKIES['participant_id']
try: quiz_game = get_object_or_404(QuizGame, join_code=join_code)
participant = QuizGameParticipant.objects.get(participant_id=participant_id) quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
if not participant.quiz_game.join_code == join_code: # Teilnehmer dem richtigen Spiel hinzufügen participant = get_object_or_404(QuizGameParticipant, participant_id=participant_id)
participant.quiz_game = quiz_game
participant.score = 0 # Reset score when joining new game if not participant.quiz_game.join_code == join_code:
participant.save() participant.quiz_game = quiz_game
except QuizGameParticipant.DoesNotExist: participant.save()
return redirect('play:create_participant', join_code=join_code)
context = {
'debug': "test",
'participant': participant,
'quiz': quiz,
}
context['participant'] = participant
return render(request, 'play/lobby.html', context=context) return render(request, 'play/lobby.html', context=context)
def create_participant(request, join_code): def create_participant(request, join_code):
quiz_game = get_object_or_404(QuizGame, join_code=join_code) if "participant_id" in request.COOKIES: # Umleiten, falls Teilnehmer bereits erstellt
return redirect('play:lobby', join_code=join_code)
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.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
if request.method == 'POST': if request.method == 'POST':
display_name = request.POST.get('display_name') display_name = request.POST.get('display_name')
if not display_name: join_code = request.POST.get('join_code')
return render(request, 'play/initialize_participant.html', { try:
'join_code': join_code, quiz = QuizGame.objects.get(join_code=join_code)
'error': 'Bitte geben Sie einen Namen ein' participant = QuizGameParticipant()
}) participant.display_name = display_name
participant.quiz_game = quiz
participant.save()
participant = QuizGameParticipant() response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code}))
participant.display_name = display_name response.set_cookie('participant_id', participant.participant_id, max_age=3600)
participant.quiz_game = quiz_game
participant.score = 0 # Initialize score
participant.save()
response = HttpResponseRedirect(reverse('play:lobby', kwargs={'join_code': join_code})) return response
response.set_cookie('participant_id', participant.participant_id, max_age=3600) except QuizGame.DoesNotExist:
return response # TODO: Fehlermeldung fuer nicht-existierendes Quiz
pass
return render(request, 'play/initialize_participant.html', {'join_code': join_code}) return render(request, 'play/initialize_participant.html', {'join_code': join_code})
def create_game(request, quiz_id):
try:
quiz = QivipQuiz.objects.get(id=quiz_id)
game = QuizGame()
game.quiz_id = quiz
game.host_id = game.generate_unique_id()
game.save()
response = HttpResponseRedirect(reverse('play:lobby', 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")
def join_game(request): def join_game(request):
if request.method == 'POST': if request.method == 'POST':
join_code = request.POST.get('game_code') join_code = request.POST.get('game_code')
try: try:
quiz = QuizGame.objects.get(join_code=join_code) 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) return redirect('play:lobby', join_code=join_code)
except QuizGame.DoesNotExist: 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') return render(request, 'play/join_game.html')
def finished(request, join_code):
try:
game = QuizGame.objects.get(join_code=join_code)
participant = None
participant_id = request.session.get('participant_id')
if participant_id:
participant = QuizGameParticipant.objects.filter(
quiz_game=game,
participant_id=participant_id
).first()
context = {
'join_code': join_code,
'is_host': str(game.host_id) == str(request.session.get('host_id')),
'participant_id': participant_id if participant else None
}
return render(request, 'play/game/finished.html', context)
except QuizGame.DoesNotExist:
return redirect('home')
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)
return render(request, 'play/game/score_overview.html', {
'quiz_game': quiz_game,
'participants': participants,
'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 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,
'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)
})
# 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/question_participant.html', {
'quiz_game': quiz_game,
'question_data': question_data,
'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
})

View File

@@ -51,74 +51,3 @@ a.qp-a-button-small {
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105 @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; 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;
}
div.qp-answer-container {
@apply grid grid-cols-2 h-screen;
}
div.qp-answer-container div {
@apply grid place-content-center rounded-xl m-2 place-self-stretch text-white transition duration-300;
@apply nth-1:bg-green-500 hover:nth-1:bg-green-600;
@apply nth-2:bg-red-500 hover:nth-2:bg-red-600;
@apply nth-3:bg-blue-500 hover:nth-3:bg-blue-600;
@apply nth-4:bg-yellow-500 hover:nth-4:bg-yellow-600;
@apply nth-5:bg-purple-500 hover:nth-5:bg-purple-600;
@apply nth-6:bg-black hover:nth-6:bg-slate-800;
}
div.qp-answer-container div p {
@apply font-black text-xl sm:text-2xl md:text-4xl;
}
div.qp-answer-container-host {
@apply grid grid-cols-2 absolute bottom-0 w-screen xl:px-20 xl:pb-10;
}
div.qp-answer-container-host div {
@apply grid place-content-center rounded-xl m-1 place-self-stretch text-white transition duration-300;
@apply nth-1:bg-green-500 hover:nth-1:bg-green-600;
@apply nth-2:bg-red-500 hover:nth-2:bg-red-600;
@apply nth-3:bg-blue-500 hover:nth-3:bg-blue-600;
@apply nth-4:bg-yellow-500 hover:nth-4:bg-yellow-600;
@apply nth-5:bg-purple-500 hover:nth-5:bg-purple-600;
@apply nth-6:bg-black hover:nth-6:bg-slate-800;
}
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;
}
div.qp-question-container img {
@apply max-w-[50vw] max-h-[40vh] mx-auto;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,67 +6,12 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/t-style.css' %}"> <link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<title>qivip</title> <title>qivip</title>
</head> </head>
<body> <body>
{% include 'partials/_nav.html' %} {% include 'partials/_nav.html' %}
{% block content%}{% endblock %} {% block content%}{% endblock %}
{% include 'partials/_footer.html' %} {% include 'partials/_footer.html' %}
<!-- Toast Container -->
<div id="toast-container" class="fixed top-4 right-4 z-50"></div>
<!-- Django Messages to Toast -->
{% if messages %}
<script>
document.addEventListener('DOMContentLoaded', function() {
{% for message in messages %}
// Use the existing toast.create function
toast.create("{{ message }}", "{{ message.tags }}");
{% endfor %}
});
</script>
{% endif %}
<script>
// WebSocket Utility
const wsUtil = {
handleClose: function(e) {
if (e.code === 1000 || e.code === 1001) {
console.log('Socket closed normally');
} else {
console.error('Socket closed unexpectedly', e.code);
}
}
};
// Toast Notification System
const toast = {
create: function(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
toast.className = `mb-4 p-4 rounded-lg shadow-lg transform transition-all duration-300 ease-in-out
${type === 'success' ? 'bg-green-100 border-l-4 border-green-500 text-green-700' :
type === 'error' ? 'bg-red-100 border-l-4 border-red-500 text-red-700' :
type === 'warning' ? 'bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700' :
'bg-blue-100 border-l-4 border-blue-500 text-blue-700'}`;
toast.innerHTML = message;
const container = document.getElementById('toast-container');
container.appendChild(toast);
// Animate in
setTimeout(() => toast.classList.add('translate-x-0', 'opacity-100'), 100);
toast.classList.add('-translate-x-4', 'opacity-0');
// Remove after duration
setTimeout(() => {
toast.classList.add('-translate-x-4', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, duration);
}
};
</script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}Antwort{% endblock %} {% block title %}Antwort{% endblock %}
{% block content %} {% block content %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}"> <link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
<!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % --> <!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % -->
{% if user.is_authenticated %} {% if user.is_authenticated %}

View File

@@ -1,5 +1,5 @@
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}"> <link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
<!doctype html> <!doctype html>
<html class="h-full"> <html class="h-full">
<head> <head>

View File

@@ -4,11 +4,11 @@
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4"> <div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
<div class="text-center"> <div class="text-center">
<h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2> <h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2>
<h3 class="mt-4 italic font-extralight sm:text-2xl text-md">Interaktives Lernen neu definiert.</h3> <h3 class="italic font-extralight sm:text-2xl text-md">Interaktives Lernen neu definiert.</h3>
</div> </div>
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md"> <div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md">
<a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a> <a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a>
<a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a> <a class="qp-a-button bg-indigo-500 text-white" href="#">Moderieren</a>
<a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a> <a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md"> <div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md">
<div class="grid place-content-center h-120"> <div class="grid place-content-center h-120">
<h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1> <h1 class="text-center m-4 text-3xl break-words lg:text-6xl">Datenschutz-<br>erklärung</b></h1>
Hier muss die Datenschutzerklärung stehen! <br> Hier muss die Datenschutzerklärung stehen! <br>
<br> <br>

View File

@@ -1,104 +1,32 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<div class="flex flex-wrap space-y-2 items-center justify-between mb-6"> <h1 class="text-2xl font-bold">{{ quiz.name }}</h1>
<h1 class="text-2xl font-bold mr-4 ">{{ quiz.name }}</h1> <div class="flex space-x-2">
<a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-4 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">&#x270F</a>
<div class=" flex flex-wrap space-x-2 "> <a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-4 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">&#x1F5D1</a>
<div class="flex flex-wrap">
{% if quiz.user_id == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</a>
</div> </div>
{% endif %}
<div class="flex flex-wrap gap-2">
<a href="{% url 'library:copy_quiz' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" />
</svg>
Quiz kopieren
</a>
<a href="{% url 'play:create_game' quiz.id %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
</div>
</div>
</div> </div>
{% if quiz.description %} {% if quiz.description %}
<p class="text-gray-600 mb-4">{{ quiz.description }}</p> <p class="text-gray-600 mb-8">{{ quiz.description }}</p>
{% endif %} {% endif %}
<div class="flex justify-end mb-4">
{% if detail == True %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false">
<div class="flex items-center mr-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
</svg>
&nbsp; Antworten verstecken
</div>
</a>
{% else %}
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true">
<div class="flex items-center mr-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
&nbsp; Antworten anzeigen
</div>
</a>
{% endif %}
</div>
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md"> <div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md">
<div class="border-b border-gray-200 p-4"> <div class="border-b border-gray-200 p-4">
<div class="flex justify-between items-center flex-wrap"> <div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">Fragen</h2> <h2 class="text-xl font-semibold">Fragen</h2>
<div class="flex space-x-2 flex-wrap"> <div class="flex space-x-2">
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}" <a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> class="bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Multiple Choice Multiple Choice
</a> </a>
{% endif %}
{% if quiz.user_id == request.user %}
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}" <a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2"> class="bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
Wahr/Falsch Wahr/Falsch
</a> </a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -109,7 +37,7 @@
{% for question in questions %} {% for question in questions %}
<div class="w-full rounded-xl p-4 hover:bg-gray-50"> <div class="w-full rounded-xl p-4 hover:bg-gray-50">
{% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" >
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex-grow"> <div class="flex-grow">
@@ -120,54 +48,49 @@
</span> </span>
</div> </div>
<div class="mt-2"> <div class="mt-2">
{% if detail %}
{% if question.data.type == 'multiple_choice' %} {% if question.data.type == 'multiple_choice' %}
<ul class="space-y-1 "> <ul class="space-y-1 ">
{% for option in question.data.options %} {% for option in question.data.options %}
<li class="flex items-center text-sm"> <li class="flex items-center text-sm">
{% if option.is_correct %} {% if option.is_correct %}
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> </svg>
<span class="font-medium text-green-700">{{ option.value }}</span> <span class="font-medium text-green-700">{{ option.value }}</span>
{% else %} {% else %}
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
</svg> </svg>
<span class="text-gray-600">{{ option.value }}</span> <span class="text-gray-600">{{ option.value }}</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% for option in question.data.options %} {% for option in question.data.options %}
{% if option.is_correct %} {% if option.is_correct %}
<p class="text-sm"> <p class="text-sm">
<span class="text-gray-500">Richtige Antwort:</span> <span class="text-gray-500">Richtige Antwort:</span>
<span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span> <span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
</p> </p>
{% endif %}
{% endfor %}
{% endif %} {% endif %}
{% endfor %}
{% endif %}
{% endif %}
</div> </div>
</div> </div>
<div class="flex h-full space-x-2 ml-4 items-center"> <div class="flex h-full space-x-2 ml-4 items-center">
<!--
<a href="{% url 'library:edit_question' question.id %}"
class="bg-gray-100 text-gray-700 px-3 py-1 rounded hover:bg-gray-200 transition-colors">
Bearbeiten
</a>
-->
<a href=" {% url 'library:delete_question' question.id %}" <a href=" {% url 'library:delete_question' question.id %}"
class=" text-red-700 px-4 py-1 rounded hover:scale-110 "> class=" text-red-700 px-4 py-1 rounded hover:scale-110 ">
{% if quiz.user_id == request.user %} &#x1F5D1
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600"> </a>
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
{% endif %}
</a>
</div> </div>
</div> </div>
@@ -187,29 +110,5 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="font-bold"> {{ quiz.user_id}}</span></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.base_quiz %}
<p class="text-gray-600 ">Autor:<span class="font-bold"> {{ quiz.base_quiz.user_id}}</span></p>
{% endif %}
</div>
<div class="flex justify-end">
{% if quiz.credits %}
<p class="text-gray-600 mb-2">Credits:<span class="font-bold"> {{ quiz.credits }}</span></p>
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -2,19 +2,10 @@
{% block content %} {% block content %}
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12"> <div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12">
<form method="post" enctype="multipart/form-data"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<div class="flex justify-center space-x-3"> <button type="submit">Speichern</button>
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Speichern
</button>
</div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,378 +1,61 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'swiper/swiper-bundle.min.css' %}">
<style>
.swiper {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="flex justify-end mr-2">
<a href="{% url 'library:new_quiz' %}" class="mt-2 p-2 shadow-md border-blue-600 border-2 rounded-md text-black mb-12">Neues Quiz erstellen</a>
</div>
{% if quizzes %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
<h1 class="font-bold mb-4 px-4 bg-blue-100">meine eigenen Quiz</h1></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
{% for quiz in quizzes %}
<div class="bg-white w-full max-w-md rounded-lg p-4 shadow-md border-blue-600 border-2 rounded-xl h-80 ">
<h2 class="text-lg font-bold break-words truncate"><a href="{% url 'library:edit_quiz' quiz.id %}">{{ quiz.name }}</a></h2>
<p class="text-sm text-gray-600 pt-11 h-54"><span class="break-words line-clamp-2">{{ quiz.description }} </span><br> Status: <span class="font-bold">{{ quiz.status }}</span>
<br> Schwierigkeit:<span class="font-bold"> {{ quiz.difficulty }}</span>
<br> Anzahl der Fragen:<span class="font-bold"> {{ quiz.question.count }}</span> </p>
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<button id="filterButton" class="p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700"> <div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="flex justify-between items-center gap-2">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> <a href="#" class="qp-a-button-small border-2 border-blue-600 text-black">Spiel starten</a>
</svg>
<span>Filter</span> <div class="flex gap-2">
</button> <a href="{% url 'library:detail_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">&#x270F;</a>
<div class="flex gap-2"> <a href="{% url 'library:delete_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">&#x1F5D1;</a>
<a href="{% url 'library:overview_quiz' %}" class="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200"> </div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> </div>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> </div>
</svg>
Zurücksetzen
</a>
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neues Quiz
</a>
</div> </div>
{% endfor %}
{% endif %}
</div> </div>
<div id="filterPanel" class="container mx-auto px-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200"> <h1 class="font-bold px-4 bg-blue-100 mt-8 mb-4">Quiz von anderen Nutzern</h1></div>
<form method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="space-y-2"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
<label class="block text-sm font-medium text-gray-700">Minimale Fragen</label> {% for quiz in all_quizzes %}
{{ form.min_amout_questions }}
<div class="h-80 bg-white w-full max-w-md rounded-lg p-4 shadow-md border-blue-600 border-2 rounded-xl ">
<h2 class="text-lg font-bold break-words truncate">{{ quiz.name }}</h2>
<p class="text-sm text-gray-600 pt-11 h-54"><span class="break-words line-clamp-2">{{ quiz.description }} </span> <br> Schwierigkeit:<span class="font-bold ">
{{ quiz.difficulty }}</span><br> Anzahl der Fragen:<span class="font-bold"> {{ quiz.question.count }}</span>
<br>Erstmalig erstellt am:<span class="font-bold"> {{ quiz.creation_date }}</span>
</p>
<div>
<div class="flex justify-between items-center gap-2">
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-black">Spiel starten</a>
<div class="flex gap-2">
<div class="qp-a-button-small text-gray-600 text-sm "> Quiz von {{ quiz.user_id }}</div>
</div>
</div> </div>
<div class="space-y-2"> </div>
<label class="block text-sm font-medium text-gray-700">Maximale Fragen</label>
{{ form.max_amout_questions }}
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Von User</label>
{{ form.user }}
</div>
<div class="md:col-span-3 flex justify-end">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
Filtern
</button>
</div>
</form>
</div> </div>
</div>
{% if user.is_authenticated %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Eigene Quiz</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in my_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="absolute inset-0 bg-cover bg-center" style="background-image: url({{ quiz.image.url }});"></div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
{% if quiz.user == request.user %}
<a href="{% url 'library:edit_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</a>
<a href="{% url 'library:delete_quiz' quiz.id %}"
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</a>
{% endif %}
</div>
</div>
</div>
</article>
{% endfor %} {% endfor %}
</div>
<!-- Pagination für eigene Quiz -->
{% if my_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if my_quizzes.has_previous %}
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
{% if my_quizzes.has_next %}
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Quiz von anderen Nutzern -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
<div class="flex items-center gap-3">
<h2 id="other-quizzes" class="text-2xl font-bold text-gray-900">Quiz von anderen Nutzern</h2>
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
</div>
</div> </div>
{% if other_quizzes %} {% endblock %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
{% for quiz in other_quizzes %}
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
<!-- Image Section (Top) -->
<div class="h-48 w-full relative flex-shrink-0">
{% if quiz.image %}
<div class="absolute inset-0 bg-cover bg-center" style="background-image: url({{ quiz.image.url }});"></div>
{% else %}
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
{% endif %}
</div>
<!-- Content Section (Bottom) -->
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
<!-- Title -->
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
</h2>
<!-- Description -->
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
<!-- Quiz Info Grid -->
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<!-- Difficulty -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-gray-800">{{ quiz.difficulty }}</span>
</div>
<!-- Questions Count -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
</div>
<!-- Status -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-gray-800">{{ quiz.status }}</span>
</div>
<!-- Rating -->
<div class="flex items-center gap-1">
{% for i in '12345'|make_list %}
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
<span class="text-yellow-500 text-base"></span>
{% else %}
<span class="text-gray-300 text-base"></span>
{% endif %}
{% endfor %}
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
</div>
</div>
<!-- Authors -->
{% if quiz.authors.all %}
<div class="flex items-center gap-2 text-sm mt-2">
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-gray-600">
{% for author in quiz.authors.all %}
{{ author.username }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</span>
</div>
{% endif %}
<!-- Action Buttons -->
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
<a href="{% url 'play:create_game' quiz.id %}"
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spielen
</a>
<div class="flex gap-1">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</a>
</div>
</div>
</div>
</article>
{% endfor %}
</div>
<!-- Pagination für andere Quiz -->
{% if other_quizzes.paginator.num_pages > 1 %}
<div class="flex justify-center space-x-2 mt-6 mb-8">
{% if other_quizzes.has_previous %}
<a href="?other_page={{ other_quizzes.previous_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a>
{% endif %}
<span class="px-3 py-1 text-gray-600">Seite {{ other_quizzes.number }} von {{ other_quizzes.paginator.num_pages }}</span>
{% if other_quizzes.has_next %}
<a href="?other_page={{ other_quizzes.next_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
<p class="text-gray-600 text-center">Keine öffentlichen Quiz gefunden.</p>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('filterButton').addEventListener('click', function() {
document.getElementById('filterPanel').classList.toggle('show');
});
});
</script>
{% endblock content %}

View File

@@ -1,29 +1,10 @@
<nav class="flex justify-between items-center bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md overflow-x-auto whitespace-nowrap"> <nav class="flex justify-between bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md">
<div class="flex items-center flex-shrink-0"> <div class="flex items-center">
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200"> <h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200"><a href="{% url 'homepage:home' %}">qivip</a></h2>
<a href="{% url 'homepage:home' %}">qivip</a>
</h2>
</div> </div>
<div class="flex items-center">
{% if show_search %}
<form method="get" class="mr-2 ml-2 flex items-center w-full max-w-sm bg-white rounded-full px-4 py-1 shadow-md">
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="w-full px-3 py-1 text-black bg-transparent focus:outline-none">
<button type="submit" class=" py-1 text-blue-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</button>
</form>
{% endif %}
<div class="flex items-center space-x-4">
<ul class="flex space-x-4 qp-nav-list"> <ul class="flex space-x-4 qp-nav-list">
{% if show_search %}
<li class="hidden md:flex"><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% else%}
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li> <li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'accounts:home' %}">Konto</a></li> <li><a href="{% url 'accounts:home' %}">Konto</a></li>
{% else %} {% else %}

View File

@@ -1,89 +0,0 @@
{% load static %}
<script>
// Gemeinsame Funktionen für alle Spielseiten
// Toast-Benachrichtigungen und WebSocket-Funktionen werden im globalen Scope definiert
window.toast = {
create(message, type = 'info') {
const toastElement = document.createElement('div');
toastElement.className = `toast toast-${type}`;
toastElement.textContent = message;
document.body.appendChild(toastElement);
// Animation
setTimeout(() => {
toastElement.classList.add('show');
setTimeout(() => {
toastElement.classList.remove('show');
setTimeout(() => toastElement.remove(), 300);
}, 3000);
}, 100);
}
};
// WebSocket-Hilfsfunktionen
window.wsUtil = {
handleClose(e) {
if (e.code !== 1000 && e.code !== 1001) {
console.error('WebSocket closed unexpectedly', e.code);
}
}
};
// WebSocket-Initialisierung
window.initializeGameSocket = function(gameCode) {
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
const gameSocket = new WebSocket(
`${wsScheme}://${window.location.host}/ws/game/${gameCode}/`
);
gameSocket.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'player_left') {
const message = data.was_kicked
? `${data.player_name} wurde wegen Inaktivität entfernt`
: `${data.player_name} hat das Spiel verlassen`;
toast.create(message, data.was_kicked ? 'error' : 'warning');
}
};
gameSocket.onclose = (e) => wsUtil.handleClose(e);
return gameSocket;
}
</script>
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 4px;
color: white;
opacity: 0;
transition: opacity 0.3s ease-in-out;
z-index: 9999;
max-width: 300px;
}
.toast.show {
opacity: 1;
}
.toast-info {
background-color: #3498db;
}
.toast-success {
background-color: #2ecc71;
}
.toast-warning {
background-color: #f1c40f;
}
.toast-error {
background-color: #e74c3c;
}
</style>

View File

@@ -1,201 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-8 mb-6">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-blue-600 mb-4">Quiz beendet!</h1>
<p class="text-xl text-gray-600">Vielen Dank fürs Mitspielen!</p>
</div>
{% if not is_host %}
<div class="bg-blue-50 rounded-lg p-6 mb-8">
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center">Wie hat dir das Quiz gefallen?</h2>
<div class="stars flex justify-center space-x-4 mb-4">
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="1"></i>
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="2"></i>
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="3"></i>
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="4"></i>
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="5"></i>
</div>
<p id="rating-message" class="text-center text-blue-600 font-medium"></p>
</div>
{% endif %}
<div class="text-center">
<a href="{% url 'homepage:home' %}"
class="inline-block px-8 py-3 text-lg font-semibold text-white bg-blue-600 rounded-full hover:bg-blue-700 transform transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Zurück zur Startseite
</a>
</div>
</div>
</div>
</div>
{% if not is_host %}
{% include 'play/game/common_scripts.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
let gameSocket = null;
let reconnectAttempts = 0;
let pingInterval = null;
const maxReconnectAttempts = 5;
const pingDelay = 30000; // 30 seconds
function startPingInterval() {
if (pingInterval) {
clearInterval(pingInterval);
}
pingInterval = setInterval(() => {
if (gameSocket && gameSocket.readyState === WebSocket.OPEN) {
gameSocket.send(JSON.stringify({ type: 'ping' }));
}
}, pingDelay);
}
function stopPingInterval() {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
function connectWebSocket() {
if (gameSocket && (gameSocket.readyState === WebSocket.CONNECTING || gameSocket.readyState === WebSocket.OPEN)) {
return gameSocket; // Already connected or connecting
}
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
document.getElementById('rating-message').textContent = 'Verbindungsfehler. Bitte Seite neu laden.';
return null;
}
try {
gameSocket = new WebSocket(
`${wsScheme}://${window.location.host}/ws/game/{{ join_code }}/`
);
gameSocket.onopen = function(e) {
console.log('Game socket connected');
reconnectAttempts = 0;
startPingInterval();
document.getElementById('rating-message').textContent = '';
};
gameSocket.onclose = function(e) {
wsUtil.handleClose(e);
stopPingInterval();
gameSocket = null;
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts) {
document.getElementById('rating-message').textContent = 'Verbindung wird wiederhergestellt...';
setTimeout(connectWebSocket, 1000 * Math.min(reconnectAttempts, 3));
}
};
gameSocket.onerror = function(e) {
console.error('Game socket error:', e);
document.getElementById('rating-message').textContent = 'Verbindungsfehler aufgetreten';
};
return gameSocket;
} catch (error) {
console.error('Error creating WebSocket:', error);
document.getElementById('rating-message').textContent = 'Verbindungsfehler aufgetreten';
return null;
}
}
const stars = document.querySelectorAll('.stars i');
let rated = false;
function setRating(rating) {
stars.forEach((star, index) => {
star.style.color = index < rating ? '#ffc107' : '#e4e5e9';
});
}
stars.forEach(star => {
star.addEventListener('mouseover', function() {
if (!rated) {
setRating(this.dataset.rating);
}
});
star.addEventListener('mouseout', function() {
if (!rated) {
setRating(0);
}
});
star.addEventListener('click', function() {
if (!rated) {
const rating = parseInt(this.dataset.rating);
rated = true;
setRating(rating);
if (!gameSocket || gameSocket.readyState !== WebSocket.OPEN) {
console.error('WebSocket is not connected');
document.getElementById('rating-message').textContent = 'Verbindung wird hergestellt...';
gameSocket = connectWebSocket();
if (gameSocket) {
const retryRating = () => {
if (gameSocket.readyState === WebSocket.OPEN) {
star.click();
} else {
setTimeout(retryRating, 500);
}
};
setTimeout(retryRating, 1000);
}
return;
}
try {
gameSocket.send(JSON.stringify({
'type': 'submit_rating',
'participant_id': '{{ participant_id }}',
'rating': rating
}));
document.getElementById('rating-message').textContent = 'Danke für deine Bewertung!';
stars.forEach(s => s.style.cursor = 'default');
} catch (error) {
console.error('Error sending rating:', error);
document.getElementById('rating-message').textContent = 'Fehler beim Senden der Bewertung';
rated = false;
}
}
});
});
// Initial connection
gameSocket = connectWebSocket();
});
</script>
<style>
.stars i {
cursor: pointer;
color: #e4e5e9;
transition: all 0.2s ease-in-out;
}
.stars i:hover ~ i {
color: #e4e5e9;
}
.stars:hover i {
color: #ffc107;
}
.stars i:hover {
transform: scale(1.2);
color: #ffc107;
}
</style>
{% endif %}
{% endblock %}

View File

@@ -1,129 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-blue-600 text-xl mb-2">Antworten</p>
<p id="answer-count" class="text-6xl font-bold text-blue-700">0/0</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
{% for option in question_data.options %}
<div class="p-4 bg-gray-100 rounded-lg" data-correct="{{ option.is_correct }}">
<p class="text-lg">{{ option.value }}</p>
<p id="option-count-{{ forloop.counter0 }}" class="text-gray-600 hidden">0 Antworten</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
<script>
const joinCode = '{{ quiz_game.join_code }}';
const hostId = '{{ host_id }}';
const questionStartTime = {{ start_time }};
const questionDuration = 30000; // 30 seconds in milliseconds
const gameSocket = initializeGameSocket(joinCode);
let answeredParticipants = 0;
let totalParticipants = 0;
const optionCounts = {};
gameSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'participant_answer') {
answeredParticipants++;
updateAnswerCount();
updateOptionCount(data.answer);
} else if (data.type === 'participant_list_update') {
totalParticipants = data.participants.length;
updateAnswerCount();
} else if (data.type === 'game_state_update' &&
data.action === 'show_scores' &&
data.redirect_url) {
window.location.href = data.redirect_url;
}
};
gameSocket.onclose = function(e) {
wsUtil.handleClose(e);
};
function updateAnswerCount() {
const answerCountElement = document.getElementById('answer-count');
if (answerCountElement) {
answerCountElement.textContent = `${answeredParticipants}/${totalParticipants}`;
}
// If everyone has answered, advance to scores
if (answeredParticipants === totalParticipants && totalParticipants > 0) {
advanceToScores();
}
}
function updateOptionCount(optionIndex) {
optionCounts[optionIndex] = (optionCounts[optionIndex] || 0) + 1;
const optionElement = document.getElementById(`option-count-${optionIndex}`);
if (optionElement) {
optionElement.textContent = `${optionCounts[optionIndex]} Antworten`;
}
}
function advanceToScores() {
// Show correct answers and answer counts
var correctOptions = document.querySelectorAll('[data-correct="True"]');
for (var i = 0; i < correctOptions.length; i++) {
correctOptions[i].classList.add('border-2', 'border-green-500');
}
// Show answer counts
var countElements = document.querySelectorAll('[id^="option-count-"]');
for (var j = 0; j < countElements.length; j++) {
countElements[j].classList.remove('hidden');
}
// Wait a moment to show the correct answer and counts
setTimeout(function() {
gameSocket.send(JSON.stringify({
'type': 'advance_to_scores',
'host_id': hostId
}));
}, 2000);
}
// Timer
function updateTimer() {
const now = Date.now();
const elapsed = now - questionStartTime;
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
document.getElementById('remaining-time').textContent = remaining;
if (remaining === 0) {
advanceToScores();
} else {
requestAnimationFrame(updateTimer);
}
}
// Request initial participants list
gameSocket.onopen = function(e) {
gameSocket.send(JSON.stringify({
'type': 'update_participants'
}));
updateTimer();
};
</script>
{% endblock %}

View File

@@ -1,115 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-end mb-4">
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Spiel verlassen
</button>
</div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
<div class="p-4 bg-blue-50 rounded-lg mb-6">
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
</div>
<div class="grid grid-cols-2 gap-4" id="options-container">
{% for option in question_data.options %}
<button
class="p-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"
data-index="{{ forloop.counter0 }}"
onclick="submitAnswer({{ forloop.counter0 }});">
<p class="text-lg">{{ option.value }}</p>
</button>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
<script>
let hasAnswered = false;
const joinCode = '{{ quiz_game.join_code }}';
const participantId = '{{ participant.participant_id }}';
const questionStartTime = {{ start_time }};
const questionDuration = 30000; // 30 seconds in milliseconds
const gameSocket = initializeGameSocket(joinCode);
gameSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'redirect' && data.url) {
window.location.href = data.url;
} else if (data.type === 'game_state_update' &&
data.action === 'show_scores' &&
data.redirect_url) {
window.location.href = data.redirect_url;
}
};
gameSocket.onclose = function(e) {
wsUtil.handleClose(e);
};
function submitAnswer(optionIndex) {
if (hasAnswered) return;
hasAnswered = true;
const now = Date.now();
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
// Disable all options and highlight selected
const options = document.getElementsByClassName('answer-option');
for (let option of options) {
option.disabled = true;
option.classList.remove('hover:bg-blue-100');
if (parseInt(option.dataset.index) === optionIndex) {
option.classList.remove('bg-gray-100');
option.classList.add('bg-blue-600', 'text-white');
} else {
option.classList.add('opacity-50');
}
}
// Send answer to server
gameSocket.send(JSON.stringify({
type: 'submit_answer',
participant_id: participantId,
answer: optionIndex,
time_remaining: timeRemaining
}));
}
// Timer
function updateTimer() {
const now = Date.now();
const elapsed = now - questionStartTime;
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
document.getElementById('remaining-time').textContent = remaining;
if (remaining === 0 && !hasAnswered) {
// Auto-submit timeout answer
submitAnswer(-1);
} else if (remaining > 0) {
requestAnimationFrame(updateTimer);
}
}
function leaveGame() {
if (confirm('Möchtest du das Spiel wirklich verlassen?')) {
gameSocket.send(JSON.stringify({
type: 'leave_game',
participant_id: participantId
}));
}
}
// Start timer when page loads
updateTimer();
</script>
{% endblock %}

View File

@@ -1,128 +1,5 @@
{% extends 'base.html' %} <!-->
Spieler sehen ihre Punktzahl, Platzierung und Richtig/Falsch
{% block content %} Host: Scoreboard, Button: 'Weiter'
<div class="container mx-auto px-4"> </!-->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
<p class="text-lg mb-2">{{ question_data.question }}</p>
<div class="grid grid-cols-2 gap-4">
{% for option in question_data.options %}
<div class="p-4 rounded-lg {% if option.is_correct %}bg-green-100 border-2 border-green-500{% else %}bg-gray-100{% endif %}">
<p class="text-lg">{{ option.value }}</p>
<p class="text-gray-600" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
</div>
{% endfor %}
</div>
</div>
<div class="mb-6">
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
<div class="space-y-2">
{% for participant in participants %}
<div class="p-4 {% if forloop.first %}bg-yellow-100 border-2 border-yellow-500{% else %}bg-gray-100{% endif %} rounded-lg flex justify-between items-center">
<div>
<p class="text-lg font-bold">{{ participant.display_name }}</p>
<p class="text-gray-600">{{ participant.score }} Punkte</p>
</div>
{% if participant.last_answer_correct %}
<span class="text-green-500"></span>
{% elif participant.last_answer_correct == False %}
<span class="text-red-500"></span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% if is_host %}
{% if is_last_question %}
<button id="finish-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Quiz beenden
</button>
{% else %}
<button id="next-button" class="w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
Nächste Frage
</button>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
<script>
const joinCode = '{{ quiz_game.join_code }}';
const hostId = '{{ host_id }}';
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
const gameSocket = new WebSocket(
`${wsScheme}://${window.location.host}/ws/game/${joinCode}/`
);
gameSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'game_state_update' &&
data.redirect_url &&
(data.action === 'next_question' || data.action === 'finish_game')) {
window.location.href = data.redirect_url;
} else if (data.type === 'answer_stats') {
updateAnswerStats(data.stats);
}
};
gameSocket.onclose = function(e) {
// Normal closure (code 1000) oder Navigation zu einer anderen Seite (code 1001)
if (e.code === 1000 || e.code === 1001) {
console.log('Game socket closed normally');
} else {
console.error('Game socket closed unexpectedly', e.code);
}
};
{% if is_host %}
// Add click handlers for host buttons
{% if is_last_question %}
var finishButton = document.getElementById('finish-button');
if (finishButton) {
finishButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'finish_game',
'host_id': hostId
}));
});
}
{% else %}
var nextButton = document.getElementById('next-button');
if (nextButton) {
nextButton.addEventListener('click', function() {
gameSocket.send(JSON.stringify({
'type': 'next_question',
'host_id': hostId
}));
});
}
{% endif %}
{% endif %}
function updateAnswerStats(stats) {
for (var optionIndex in stats) {
if (stats.hasOwnProperty(optionIndex)) {
var element = document.getElementById('option-count-' + optionIndex);
if (element) {
element.textContent = stats[optionIndex] + ' Antworten';
}
}
}
}
// Request answer stats when page loads
gameSocket.onopen = function(e) {
gameSocket.send(JSON.stringify({
'type': 'get_answer_stats'
}));
};
</script>
{% endblock %}

View File

@@ -1,53 +1,5 @@
{% extends 'base.html' %} <!-->
Spieler: Countdown, automatische Weiterleitung
{% block content %} (Host: Frage)
<div class="container mx-auto px-4"> </!-->
<div class="bg-white rounded-lg shadow-md p-6 mb-6 text-center">
<h1 class="text-3xl font-bold mb-6">Warte auf andere Spieler...</h1>
<div class="p-4 bg-blue-50 rounded-lg mb-6 inline-block">
<p class="text-blue-600 text-xl mb-2">Die nächste Frage beginnt in</p>
<p id="countdown" class="text-6xl font-bold text-blue-700">5</p>
</div>
<div class="animate-pulse">
<p class="text-gray-600">Bitte warte, bis alle Spieler bereit sind</p>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% include 'play/game/common_scripts.html' %}
<script>
const joinCode = '{{ quiz_game.join_code }}';
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
const gameSocket = new WebSocket(
wsScheme + '://' + window.location.host + '/ws/game/' + joinCode + '/'
);
let countdown = 5;
const countdownElement = document.getElementById('countdown');
gameSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'game_state_update' && data.action === 'start_question' && data.redirect_url) {
window.location.href = data.redirect_url;
}
};
gameSocket.onclose = function(e) {
wsUtil.handleClose(e);
};
function updateCountdown() {
countdownElement.textContent = countdown;
if (countdown > 0) {
countdown--;
setTimeout(updateCountdown, 1000);
}
}
updateCountdown();
</script>
{% endblock %}

View File

@@ -1,63 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <h1>User for a Game:</h1>
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6"> <div class="input-group border-blue-600 border-2 rounded-xl shadow-md">
<h1 class="text-2xl font-bold mb-6 text-center text-blue-600">Spieler einrichten</h1> <form method="post">
{% csrf_token %}
<div class="text-center mb-6"> <input type="hidden" name="join_code" value="{{join_code}}">
<div class="inline-block bg-blue-100 rounded-lg px-4 py-2"> <input type="text" name="display_name" placeholder="Anzeigename">
<span class="text-sm text-blue-800">Spiel-Code:</span> <button type="submit">Speichern</button>
<span class="font-mono font-bold text-blue-900">{{ join_code }}</span> </form>
</div>
</div>
<form method="post" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="join_code" value="{{join_code}}">
<div class="bg-gray-50 p-4 rounded-lg mb-4">
<p class="text-sm text-gray-600 mb-2">Wähle einen Anzeigenamen für das Spiel:</p>
<input type="text"
name="display_name"
required
maxlength="20"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Dein Spielername">
<p class="text-xs text-gray-500 mt-2">Maximal 20 Zeichen</p>
</div>
<div class="flex justify-center space-x-4">
<a href="/"
class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200">
Abbrechen
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors duration-200">
Spiel beitreten
</button>
</div>
</form>
{% if error %}
<div class="mt-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700">
<p class="font-medium">Fehler</p>
<p class="text-sm">{{ error }}</p>
</div>
{% endif %}
</div>
</div> </div>
<style>
input:focus {
outline: none;
}
.container {
min-height: calc(100vh - 4rem);
display: flex;
align-items: center;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="sm:grid sm:place-items-center sm:min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2"> <div class="grid place-items-center min-h-[calc(100vh-5rem)] py-6 m-2">
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white"> <div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white">
<h1 class="text-2xl text-center font-bold mb-4">Spiel beitreten</h1> <h1 class="text-2xl text-center font-bold mb-4">Spiel beitreten</h1>
<form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4"> <form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4">
@@ -15,13 +15,6 @@
</svg> </svg>
</button> </button>
</div> </div>
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
</form> </form>
<script> <script>
const input = document.querySelector('input[name="game_code"]'); const input = document.querySelector('input[name="game_code"]');

View File

@@ -1,372 +1,21 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4">
<h1>{{ quiz.name }}</h1>
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6"> <div class="mt-4 mb-4 text-center">
<!-- Quiz Info --> <h3>Sichtbar als <b>{{ participant.display_name }}</b></h3>
<div class="text-center mb-8">
<h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3>
<div class="bg-blue-100 rounded-lg p-4 mb-4">
<h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1>
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p>
</div>
</div>
<!-- Participant Info -->
{% if participant %}
<div class="mb-6 text-center">
<div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
<span>Angemeldet als <b>{{ participant.display_name }}</b></span>
</div>
</div>
{% endif %}
<!-- Participants List -->
<div class="mb-6">
<h3 class="font-bold text-lg mb-3 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
Teilnehmer
</h3>
<ul id="participants-list" class="space-y-2 max-h-60 overflow-y-auto">
<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Warte auf Teilnehmer...</li>
</ul>
</div>
<!-- Action Buttons -->
<div class="space-y-3">
{% if host_id %}
<button id="start-button" class="w-full p-4 rounded-lg bg-blue-600 text-white font-bold hover:bg-blue-700 transform hover:scale-105 transition duration-200 shadow-md flex items-center justify-center" type="button">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Spiel starten
</button>
{% endif %}
{% if participant %}
<button id="leave-button" class="w-full p-4 rounded-lg bg-red-100 text-red-700 font-bold hover:bg-red-200 transition duration-200 flex items-center justify-center" type="button">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Spiel verlassen
</button>
{% endif %}
</div>
</div> </div>
<div id="player-list">
<ul>
<li>Noch keine Spieler gefunden.</li>
</ul>
{{debug}}
</div>
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit">Starten</button>
</div> </div>
{% endblock %}
{% block extra_js %}
<script> <script>
const joinCode = '{{ quiz_game.join_code }}'; // TODO: Websocket Verbindung aufbauen und Teilnehmerliste bei beitreten updaten
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
let lobbySocket = null;
let reconnectAttempts = 0;
let pingInterval = null;
const maxReconnectAttempts = 5;
const pingDelay = 30000; // 30 seconds
const heartbeatDelay = 10000; // 10 seconds
let heartbeatInterval = null;
function startPingInterval() {
stopPingInterval();
pingInterval = setInterval(() => {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
lobbySocket.send(JSON.stringify({ type: 'ping' }));
}
}, pingDelay);
}
function stopPingInterval() {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
function startHeartbeat() {
stopHeartbeat();
heartbeatInterval = setInterval(() => {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
lobbySocket.send(JSON.stringify({
type: 'heartbeat',
participant_id: '{{ participant.participant_id }}'
}));
}
}, heartbeatDelay);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
function connectWebSocket() {
if (lobbySocket && (lobbySocket.readyState === WebSocket.CONNECTING || lobbySocket.readyState === WebSocket.OPEN)) {
return lobbySocket; // Already connected or connecting
}
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return null;
}
try {
lobbySocket = new WebSocket(
wsScheme + '://' + window.location.host + '/ws/play/lobby/' + joinCode + '/'
);
lobbySocket.onopen = function(e) {
console.log('Lobby socket connected');
reconnectAttempts = 0;
startPingInterval();
{% if participant %}
startHeartbeat();
{% endif %}
// Request initial participants list
lobbySocket.send(JSON.stringify({
type: 'update_participants'
}));
};
lobbySocket.onclose = function(e) {
wsUtil.handleClose(e);
stopPingInterval();
stopHeartbeat();
lobbySocket = null;
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000); // Exponential backoff, max 10s
setTimeout(connectWebSocket, delay);
}
};
lobbySocket.onerror = function(e) {
console.error('Lobby socket error:', e);
};
lobbySocket.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'participants_list_update') {
updateParticipantsList(data.participants);
} else if (data.type === 'game_state_update' &&
data.action === 'start_game' &&
data.redirect_url) {
window.location.href = data.redirect_url;
} else if (data.type === 'pong') {
console.log('Received pong from server');
} else if (data.type === 'redirect' && data.url) {
window.location.href = data.url;
} else if (data.type === 'player_joined' && '{{ host_id }}') {
toast.create(`${data.player_name} ist beigetreten`, 'success');
} else if (data.type === 'player_left') {
if (data.was_kicked) {
// Show kick message
toast.create(`${data.player_name} wurde vom Host entfernt`, 'error');
// If this client was kicked, redirect to home
if (data.participant_id === '{{ participant.participant_id }}') {
window.location.href = '/';
}
} else {
toast.create(`${data.player_name} hat das Spiel verlassen`, 'info');
}
}
};
return lobbySocket;
} catch (error) {
console.error('Error creating WebSocket:', error);
return null;
}
}
// Initial connection
lobbySocket = connectWebSocket();
function leaveGame() {
if (confirm('Möchtest du das Spiel wirklich verlassen?')) {
lobbySocket.send(JSON.stringify({
type: 'leave_game',
participant_id: '{{ participant.participant_id }}'
}));
}
}
function kickPlayer(participantId) {
const button = event.currentTarget;
if (!button.disabled && confirm('Möchtest du diesen Spieler wirklich aus dem Spiel entfernen?')) {
console.log('Attempting to kick player:', participantId);
console.log('Host ID:', '{{ quiz_game.host_id }}');
console.log('Current host cookie:', document.cookie.split('; ').find(row => row.startsWith('host_id=')));
button.disabled = true;
button.classList.add('opacity-50');
button.innerHTML = '<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
'</svg>';
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
const message = {
type: 'kick_player',
participant_id: participantId,
host_id: '{{ quiz_game.host_id }}'
};
console.log('Sending kick message:', message);
lobbySocket.send(JSON.stringify(message));
} else {
console.error('WebSocket is not connected');
button.disabled = false;
button.classList.remove('opacity-50');
alert('Verbindungsfehler. Bitte versuche es erneut.');
}
}
}
function updateParticipantsList(participants) {
const list = document.getElementById('participants-list');
if (!list) {
console.warn('Participants list element not found');
return;
}
if (!participants || participants.length === 0) {
list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Noch keine Teilnehmer...</li>';
return;
}
try {
// Finde neue und entfernte Teilnehmer
const currentParticipants = Array.from(list.children)
.map(li => li.textContent)
.filter(name => name !== 'Noch keine Teilnehmer...' && name !== 'Fehler beim Aktualisieren der Liste');
const newParticipants = participants
.filter(p => !currentParticipants.includes(p.display_name))
.map(p => p.display_name);
const removedParticipants = currentParticipants
.filter(name => !participants.find(p => p.display_name === name));
// Aktualisiere die Liste mit animierten Einträgen
list.innerHTML = participants
.filter(p => p && p.display_name) // Filter out invalid participants
.map(function(p) {
if (!p || !p.display_name) {
console.error('Invalid participant data:', p);
return '';
}
const displayName = p.display_name;
const initial = displayName.charAt(0) || '?';
const participantId = p.participant_id;
// Check if this entry is for the host
const isHost = participantId === '{{ quiz_game.host_id }}';
const isCurrentUserHost = document.cookie.includes('host_id={{ quiz_game.host_id }}');
console.log('Participant check:', {
participantId,
displayName,
isHost,
isCurrentUserHost,
gameHostId: '{{ quiz_game.host_id }}'
});
// Show kick button if current user is host and this is not their own entry
let kickButton = '';
if (isCurrentUserHost && !isHost && participantId) {
kickButton = '<button onclick="kickPlayer(\'' + p.participant_id + '\')" ' +
'class="p-1.5 rounded-full text-red-600 hover:text-white hover:bg-red-600 ' +
'transition-all duration-200 group relative" title="Spieler kicken">' +
'<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' +
'd="M6 18L18 6M6 6l12 12"/>' +
'</svg>' +
'<span class="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' +
'bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 ' +
'transition-opacity duration-200 whitespace-nowrap">Spieler kicken</span>' +
'</button>';
}
return '<li class="p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' +
'<div class="flex items-center">' +
'<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">' +
'<span class="text-blue-600 font-bold">' + initial.toUpperCase() + '</span>' +
'</div>' +
'<span class="font-medium">' + displayName + '</span>' +
'</div>' +
'<div class="flex items-center space-x-2">' +
(isHost ? '<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">Host</span>' : '') +
'<span class="w-2 h-2 bg-green-500 rounded-full ml-2"></span>' +
kickButton +
'</div>' +
'</li>';
})
.join('');
// Benachrichtigungen werden über WebSocket-Events gehandelt
} catch (error) {
console.error('Error updating participants list:', error);
list.innerHTML = '<li class="text-red-500">Fehler beim Aktualisieren der Liste</li>';
}
}
{% if host_id %}
const startButton = document.getElementById('start-button');
if (startButton) {
startButton.addEventListener('click', function() {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
startButton.disabled = true;
startButton.classList.add('opacity-50');
startButton.innerHTML =
'<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
'</svg>' +
'Spiel wird gestartet...';
lobbySocket.send(JSON.stringify({
type: 'start_game',
host_id: '{{ host_id }}'
}));
}
});
}
{% endif %}
{% if participant %}
const leaveButton = document.getElementById('leave-button');
if (leaveButton) {
leaveButton.addEventListener('click', function() {
if (lobbySocket && lobbySocket.readyState === WebSocket.OPEN) {
leaveButton.disabled = true;
leaveButton.classList.add('opacity-50');
leaveButton.innerHTML =
'<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
'</svg>' +
'Verlasse Spiel...';
lobbySocket.send(JSON.stringify({
type: 'leave_game',
participant_id: '{{ participant.participant_id }}'
}));
// Warte kurz und leite dann zur Startseite weiter
setTimeout(() => {
window.location.href = '/';
}, 500);
}
});
}
{% endif %}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,3 @@
# Core dependencies
django~=5.1.4 django~=5.1.4
channels~=4.2.0 # WebSocket support channels~=4.2.0
daphne~=4.1.2 # ASGI server daphne~=4.1.2
pillow~=11.1.0 # Image handling
whitenoise~=6.6.0 # Static file serving