diff --git a/django/library/migrations/0031_alter_qivipquestion_quiz_id.py b/django/library/migrations/0031_alter_qivipquestion_quiz_id.py new file mode 100644 index 0000000..fc46a2f --- /dev/null +++ b/django/library/migrations/0031_alter_qivipquestion_quiz_id.py @@ -0,0 +1,19 @@ +# 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'), + ), + ] diff --git a/django/library/migrations/0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more.py b/django/library/migrations/0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more.py new file mode 100644 index 0000000..2c3d06a --- /dev/null +++ b/django/library/migrations/0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more.py @@ -0,0 +1,37 @@ +# 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')}, + }, + ), + ] diff --git a/django/play/consumers/__init__.py b/django/play/consumers/__init__.py new file mode 100644 index 0000000..7e9d7f6 --- /dev/null +++ b/django/play/consumers/__init__.py @@ -0,0 +1,3 @@ +from .game import GameConsumer + +__all__ = ['GameConsumer'] diff --git a/django/play/consumers/game.py b/django/play/consumers/game.py new file mode 100644 index 0000000..841a8a9 --- /dev/null +++ b/django/play/consumers/game.py @@ -0,0 +1,492 @@ +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}) + } + ) diff --git a/django/play/consumers/lobby.py b/django/play/consumers/lobby.py new file mode 100644 index 0000000..2add1ca --- /dev/null +++ b/django/play/consumers/lobby.py @@ -0,0 +1,285 @@ +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 per game +game_check_tasks = {} + +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(30) # Check every 30 seconds + # 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 + active_participants = await database_sync_to_async(lambda: list( + game.participants.filter(last_heartbeat__gte=cutoff_time) + .values_list('display_name', flat=True) + ))() + + # 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 + 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') + + # 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() + + # 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( + self.room_group_name, + self.channel_name + ) + + 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) + # Update participants list after heartbeat + await self.update_participants() + 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(minutes=1) + 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)) + + diff --git a/django/play/migrations/0006_quizgame_current_question_index_and_more.py b/django/play/migrations/0006_quizgame_current_question_index_and_more.py new file mode 100644 index 0000000..693cb23 --- /dev/null +++ b/django/play/migrations/0006_quizgame_current_question_index_and_more.py @@ -0,0 +1,28 @@ +# 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), + ), + ] diff --git a/django/play/migrations/0007_alter_quizgameparticipant_quiz_game.py b/django/play/migrations/0007_alter_quizgameparticipant_quiz_game.py new file mode 100644 index 0000000..ed911fc --- /dev/null +++ b/django/play/migrations/0007_alter_quizgameparticipant_quiz_game.py @@ -0,0 +1,19 @@ +# 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'), + ), + ] diff --git a/django/play/migrations/0008_quizgameparticipant_last_answer_and_more.py b/django/play/migrations/0008_quizgameparticipant_last_answer_and_more.py new file mode 100644 index 0000000..ab3ad74 --- /dev/null +++ b/django/play/migrations/0008_quizgameparticipant_last_answer_and_more.py @@ -0,0 +1,23 @@ +# 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), + ), + ] diff --git a/django/play/migrations/0009_quizanswer.py b/django/play/migrations/0009_quizanswer.py new file mode 100644 index 0000000..02f8c26 --- /dev/null +++ b/django/play/migrations/0009_quizanswer.py @@ -0,0 +1,29 @@ +# 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')}, + }, + ), + ] diff --git a/django/templates/play/game/common_scripts.html b/django/templates/play/game/common_scripts.html new file mode 100644 index 0000000..2c59b37 --- /dev/null +++ b/django/templates/play/game/common_scripts.html @@ -0,0 +1,89 @@ +{% load static %} + + + + diff --git a/django/templates/play/game/finished.html b/django/templates/play/game/finished.html new file mode 100644 index 0000000..b0300e1 --- /dev/null +++ b/django/templates/play/game/finished.html @@ -0,0 +1,201 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+

Quiz beendet!

+

Vielen Dank fürs Mitspielen!

+
+ + {% if not is_host %} +
+

Wie hat dir das Quiz gefallen?

+
+ + + + + +
+

+
+ {% endif %} + + +
+
+
+ +{% if not is_host %} +{% include 'play/game/common_scripts.html' %} + + + +{% endif %} +{% endblock %}