diff --git a/django/core/settings.py b/django/core/settings.py index 65b017c..f8244b3 100644 --- a/django/core/settings.py +++ b/django/core/settings.py @@ -98,6 +98,9 @@ STORAGES = { "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, } diff --git a/django/library/views.py b/django/library/views.py index 0211216..36d77b9 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -15,20 +15,20 @@ from django.contrib.auth.models import User # Übersicht aller Quizze def overview_quiz(request): - - #Filter + # Filter form = QuizFilterForm(request.GET) - - try: - - filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions')) - except: + # Initialize querysets + 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.annotate(max_amout_questions=Count('questions')) - - + 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') @@ -41,10 +41,7 @@ def overview_quiz(request): if min_amout_questions: filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=min_amout_questions) - try: - filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions) - except: - filter_my_quizzes = QivipQuiz.objects.none() + 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) @@ -69,15 +66,26 @@ def overview_quiz(request): - filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1).filter(status='öffentlich') - + # 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, - 'filter_my_quizzes': filter_my_quizzes.order_by('-creation_date'), - 'filter_other_quizzes': filter_other_quizzes.order_by('-creation_date'), + 'my_quizzes': my_quizzes, + 'other_quizzes': other_quizzes, 'form': form, } - return render(request, 'library/overview_quiz.html', context) diff --git a/django/play/consumers/lobby.py b/django/play/consumers/lobby.py index 2add1ca..cccbd9f 100644 --- a/django/play/consumers/lobby.py +++ b/django/play/consumers/lobby.py @@ -4,8 +4,9 @@ import asyncio from ..models import QuizGame, QuizGameParticipant from channels.db import database_sync_to_async -# Dictionary to track inactive check tasks per game +# Dictionary to track inactive check tasks and active connections per game game_check_tasks = {} +game_connections = {} class LobbyConsumer(AsyncWebsocketConsumer): @classmethod @@ -16,39 +17,20 @@ class LobbyConsumer(AsyncWebsocketConsumer): room_group_name = f'lobby_{join_code}' while True: try: - await asyncio.sleep(30) # Check every 30 seconds + 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(minutes=1) - # Get previous active participants - prev_active = await database_sync_to_async(lambda: set( - game.participants.values_list('display_name', flat=True) - ))() - - # Get current active participants + 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_list('display_name', flat=True) + .values('participant_id', 'display_name') ))() - # Find removed participants - removed = set(prev_active) - set(active_participants) - - # Send notifications for removed participants - for name in removed: - await channel_layer.group_send( - room_group_name, - { - 'type': 'player_left', - 'player_name': name, - 'was_kicked': True - } - ) - - # Broadcast update to all clients + # Send update to all clients await channel_layer.group_send( room_group_name, { @@ -76,6 +58,14 @@ class LobbyConsumer(AsyncWebsocketConsumer): # 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, @@ -87,9 +77,6 @@ class LobbyConsumer(AsyncWebsocketConsumer): # Send initial participants list await self.update_participants() - # 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 room group await self.channel_layer.group_discard( @@ -97,6 +84,17 @@ class LobbyConsumer(AsyncWebsocketConsumer): 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') @@ -113,8 +111,7 @@ class LobbyConsumer(AsyncWebsocketConsumer): participant_id = data.get('participant_id') if participant_id: await self.update_participant_heartbeat(participant_id) - # Update participants list after heartbeat - await self.update_participants() + # 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: @@ -200,7 +197,7 @@ class LobbyConsumer(AsyncWebsocketConsumer): 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(minutes=1) + 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') diff --git a/django/play/migrations/0010_alter_quizgameparticipant_last_heartbeat.py b/django/play/migrations/0010_alter_quizgameparticipant_last_heartbeat.py new file mode 100644 index 0000000..04f5530 --- /dev/null +++ b/django/play/migrations/0010_alter_quizgameparticipant_last_heartbeat.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/django/play/models.py b/django/play/models.py index adf9fee..33feaf1 100644 --- a/django/play/models.py +++ b/django/play/models.py @@ -43,7 +43,7 @@ class QuizGameParticipant(models.Model): quiz_game = models.ForeignKey(QuizGame, on_delete=models.CASCADE, related_name="participants") score = models.IntegerField(verbose_name="Punkte", default=0) avatar = models.CharField(max_length=200, blank=True, default="") - last_heartbeat = models.DateTimeField(auto_now=True) + 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) diff --git a/django/templates/library/overview_quiz.html b/django/templates/library/overview_quiz.html index 9278100..10d9a5e 100644 --- a/django/templates/library/overview_quiz.html +++ b/django/templates/library/overview_quiz.html @@ -1,236 +1,392 @@ {% extends 'base.html' %} -{% block content %} {% load static %} -
- - - - +{% endblock %} - .swiper-slide { - text-align: center; - font-size: 18px; - background: #fff; - display: flex; - justify-content: center; - align-items: center; - } +{% block content %} - - - - - -