Compare commits
26 Commits
91-bilder-
...
88-avatare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c42ecf13 | ||
|
|
cc8cc9ad74 | ||
|
|
0c8a8d3062 | ||
|
|
a021e7561a | ||
|
|
9321b44056 | ||
|
|
873b50103d | ||
|
|
41cecf98dc | ||
|
|
ac5859a666 | ||
|
|
779762c2a3 | ||
|
|
da78060415 | ||
|
|
b823909386 | ||
|
|
82f0cd5191 | ||
|
|
1069dd1cad | ||
|
|
905da6737c | ||
|
|
6eed8107ac | ||
|
|
95e4428943 | ||
|
|
bad257f341 | ||
|
|
eaf709ed4d | ||
|
|
8ca470ad54 | ||
|
|
af919b9ea5 | ||
|
|
154110e510 | ||
|
|
a16c97c4f3 | ||
|
|
01562c6ad8 | ||
|
|
50cd95daf1 | ||
|
|
1a4b9e0d6b | ||
|
|
867c839202 |
@@ -7,6 +7,7 @@
|
|||||||
3. [Superuser erstellen](#superuser-erstellen)
|
3. [Superuser erstellen](#superuser-erstellen)
|
||||||
4. [Tailwind-Nutzung](#tailwind-nutzung)
|
4. [Tailwind-Nutzung](#tailwind-nutzung)
|
||||||
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
|
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
|
||||||
|
6. [Konfiguration](#konfiguration)
|
||||||
|
|
||||||
## Initialisierung des Projekts
|
## Initialisierung des Projekts
|
||||||
|
|
||||||
@@ -161,3 +162,11 @@ Hier der vereinbarte Arbeitsablauf:
|
|||||||
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
|
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
|
||||||
|
|
||||||
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
|
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
|
||||||
|
|
||||||
|
| Variable | Bedeutung | Optionen |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
|
||||||
3
django/config/qivip_config.json
Normal file
3
django/config/qivip_config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ENABLE_RATING_SYSTEM": true
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
@@ -27,6 +28,9 @@ DEBUG = True
|
|||||||
|
|
||||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
|
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
|
||||||
|
|
||||||
|
# Konfigurationsdatei für Projekt
|
||||||
|
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
|
||||||
|
QIVIP_CONFIG = json.load(config_file)
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuizCategory
|
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage
|
||||||
|
|
||||||
# Für das QivipQuiz Modell
|
# Für das QivipQuiz Modell
|
||||||
class QivipQuizAdmin(admin.ModelAdmin):
|
class QivipQuizAdmin(admin.ModelAdmin):
|
||||||
@@ -23,8 +23,17 @@ class QuizCategoryAdmin(admin.ModelAdmin):
|
|||||||
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
|
class QivipQuizFavoriteAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
|
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden
|
||||||
|
|
||||||
|
|
||||||
|
class QuizImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('image',)
|
||||||
|
|
||||||
|
class QuestionImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('image',)
|
||||||
|
|
||||||
# Registrierung der Modelle im Admin
|
# Registrierung der Modelle im Admin
|
||||||
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(QivipQuizFavorite, QivipQuizFavoriteAdmin)
|
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
|
||||||
|
admin.site.register(QuizImage, QuizImageAdmin)
|
||||||
|
admin.site.register(QuestionImage, QuestionImageAdmin)
|
||||||
@@ -4,7 +4,7 @@ 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','difficulty','credits']
|
||||||
|
|
||||||
class QuestionForm(forms.ModelForm):
|
class QuestionForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-04-22 16:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0049_alter_qivipquestion_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuestionImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuizImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquestion',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='library.questionimage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquiz',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.QuizImage'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-04-23 12:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0050_questionimage_quizimages_alter_qivipquestion_image_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquestion',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.questionimage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquiz',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='library.quizimage'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-04-23 12:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0051_alter_qivipquestion_image_alter_qivipquiz_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquestion',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquiz',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='QuestionImage',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='QuizImage',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-04-23 13:15
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0052_alter_qivipquestion_image_alter_qivipquiz_image_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuestionImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='QuizImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquestion',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='library.questionimage'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='qivipquiz',
|
||||||
|
name='image',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='quizzes', to='library.quizimage'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,6 +4,15 @@ from django.utils.text import slugify
|
|||||||
import uuid
|
import uuid
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class QuizImage(models.Model):
|
||||||
|
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionImage(models.Model):
|
||||||
|
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
class QivipQuiz(models.Model):
|
class QivipQuiz(models.Model):
|
||||||
STATUS_VALUES = {
|
STATUS_VALUES = {
|
||||||
"öffentlich": "Öffentlich",
|
"öffentlich": "Öffentlich",
|
||||||
@@ -27,7 +36,15 @@ 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,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
|
image = models.ForeignKey(
|
||||||
|
QuizImage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="quizzes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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')
|
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
|
||||||
@@ -74,7 +91,14 @@ class QivipQuestion(models.Model):
|
|||||||
update_date = models.DateTimeField(auto_now=True)
|
update_date = models.DateTimeField(auto_now=True)
|
||||||
data = models.TextField()
|
data = models.TextField()
|
||||||
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
|
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
|
||||||
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
|
image = models.ForeignKey(
|
||||||
|
QuestionImage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="questions"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.data[:50]
|
return self.data[:50]
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite
|
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizImage
|
||||||
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
|
||||||
@@ -193,10 +193,23 @@ def favorite_quiz(request, pk):
|
|||||||
# 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, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
quiz = form.save(commit=False)
|
quiz = form.save(commit=False)
|
||||||
|
try:
|
||||||
|
quiz_image=request.FILES['quiz_image']
|
||||||
|
imagequiz=QuizImage.objects.create(image=quiz_image)
|
||||||
|
quiz.image=imagequiz
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
quiz.user_id = request.user
|
quiz.user_id = request.user
|
||||||
#quiz.creator = request.user
|
#quiz.creator = request.user
|
||||||
quiz.save()
|
quiz.save()
|
||||||
@@ -214,8 +227,28 @@ def edit_quiz(request, pk):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
|
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
|
if 'reset_image' in request.POST:
|
||||||
|
quiz.image = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
quiz_image=request.FILES['quiz_image']
|
||||||
|
imagequiz=QuizImage.objects.create(image=quiz_image)
|
||||||
|
quiz.image=imagequiz
|
||||||
|
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
form.save()
|
form.save()
|
||||||
|
for img in QuizImage.objects.all():
|
||||||
|
try:
|
||||||
|
img.delete()
|
||||||
|
img.image.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return redirect('library:detail_quiz', pk=pk)
|
return redirect('library:detail_quiz', pk=pk)
|
||||||
#return modified(request, pk)
|
#return modified(request, pk)
|
||||||
|
|
||||||
@@ -229,6 +262,19 @@ def delete_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':
|
||||||
quiz.delete()
|
quiz.delete()
|
||||||
|
for img in QuizImage.objects.all():
|
||||||
|
try:
|
||||||
|
img.delete()
|
||||||
|
img.image.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
for img in QuestionImage.objects.all():
|
||||||
|
try:
|
||||||
|
img.delete()
|
||||||
|
img.image.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return redirect('library:overview_quiz')
|
return redirect('library:overview_quiz')
|
||||||
return render(request, 'library/delete_confirmation.html', {'object': quiz})
|
return render(request, 'library/delete_confirmation.html', {'object': quiz})
|
||||||
|
|
||||||
@@ -254,6 +300,7 @@ def detail_quiz(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
def copy_quiz(request, pk):
|
def copy_quiz(request, pk):
|
||||||
original_quiz = get_object_or_404(QivipQuiz, pk=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)
|
||||||
@@ -264,11 +311,10 @@ def copy_quiz(request, pk):
|
|||||||
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
|
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.uuid = uuid.uuid4() # Neue UUID generieren
|
||||||
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
|
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
|
||||||
new_quiz.name += " - Kopie" # Optional: Name anpassen
|
new_quiz.name += " - Kopie"
|
||||||
new_quiz.base_quiz_id = pk
|
new_quiz.base_quiz_id = pk
|
||||||
new_quiz.rating_count = 0
|
new_quiz.rating_count = 0
|
||||||
new_quiz.average_rating = 3.0
|
new_quiz.average_rating = 3.0
|
||||||
#new_quiz.creator = original_quiz.creator or request.user
|
|
||||||
new_quiz.save() # Speichern als neues Objekt
|
new_quiz.save() # Speichern als neues Objekt
|
||||||
# Optional: Beschreibung anpassen
|
# Optional: Beschreibung anpassen
|
||||||
# Alle zugehörigen Fragen kopieren
|
# Alle zugehörigen Fragen kopieren
|
||||||
@@ -278,6 +324,7 @@ def copy_quiz(request, pk):
|
|||||||
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
|
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.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.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
|
||||||
|
|
||||||
question.save()
|
question.save()
|
||||||
|
|
||||||
messages.success(request, "Quiz wurde erfolgreich kopiert!")
|
messages.success(request, "Quiz wurde erfolgreich kopiert!")
|
||||||
@@ -315,10 +362,9 @@ def new_question(request):
|
|||||||
if question_type not in ['multiple_choice', 'true_false']:
|
if question_type not in ['multiple_choice', 'true_false']:
|
||||||
base_url = reverse('library:new_question')
|
base_url = reverse('library:new_question')
|
||||||
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
question_text = request.POST.get('question', '')
|
question_text = request.POST.get('question', '')
|
||||||
question_image = request.FILES.get('question_image', None)
|
|
||||||
|
|
||||||
# Handle different question types
|
# Handle different question types
|
||||||
if question_type == 'true_false':
|
if question_type == 'true_false':
|
||||||
@@ -358,7 +404,14 @@ def new_question(request):
|
|||||||
question = QivipQuestion()
|
question = QivipQuestion()
|
||||||
question.data = json.dumps(json_data)
|
question.data = json.dumps(json_data)
|
||||||
question.quiz_id = quiz
|
question.quiz_id = quiz
|
||||||
question.image = question_image
|
try:
|
||||||
|
question_image=request.FILES['question_image']
|
||||||
|
imagequestion=QuestionImage.objects.create(image=question_image)
|
||||||
|
question.image=imagequestion
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||||
question.save()
|
question.save()
|
||||||
#return modified(request, pk=quiz.pk)
|
#return modified(request, pk=quiz.pk)
|
||||||
@@ -400,6 +453,8 @@ def new_question(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def edit_question(request, pk):
|
def edit_question(request, pk):
|
||||||
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
|
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
|
||||||
|
if 'reset_ques_image' in request.POST:
|
||||||
|
question.image = None
|
||||||
if question.quiz_id.user_id != request.user:
|
if question.quiz_id.user_id != request.user:
|
||||||
return redirect('library:question_list')
|
return redirect('library:question_list')
|
||||||
|
|
||||||
@@ -453,7 +508,9 @@ def edit_question(request, pk):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
question_text = request.POST.get('question', '')
|
question_text = request.POST.get('question', '')
|
||||||
try:
|
try:
|
||||||
question_image = request.FILES['question_image']
|
question_image=request.FILES['question_image']
|
||||||
|
imagequestion=QuestionImage.objects.create(image=question_image)
|
||||||
|
question.image=imagequestion
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -498,8 +555,15 @@ def edit_question(request, pk):
|
|||||||
|
|
||||||
question.data = json.dumps(json_data)
|
question.data = json.dumps(json_data)
|
||||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||||
question.image = question_image
|
|
||||||
|
|
||||||
question.save()
|
question.save()
|
||||||
|
for img in QuestionImage.objects.all():
|
||||||
|
try:
|
||||||
|
img.delete()
|
||||||
|
img.image.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
#return modified(request, question.quiz_id.pk)
|
#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)
|
||||||
|
|
||||||
@@ -527,5 +591,11 @@ def delete_question(request, pk):
|
|||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
question.delete()
|
question.delete()
|
||||||
|
for img in QuestionImage.objects.all():
|
||||||
|
try:
|
||||||
|
img.delete()
|
||||||
|
img.image.delete(save=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
|
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
|
||||||
return render(request, 'library/delete_confirmation.html', {'object': question})
|
return render(request, 'library/delete_confirmation.html', {'object': question})
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ from channels.generic.websocket import AsyncWebsocketConsumer
|
|||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
class GameConsumer(AsyncWebsocketConsumer):
|
class GameConsumer(AsyncWebsocketConsumer):
|
||||||
|
|
||||||
@@ -244,6 +242,8 @@ class GameConsumer(AsyncWebsocketConsumer):
|
|||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def save_rating(self, participant_id, rating):
|
def save_rating(self, participant_id, rating):
|
||||||
|
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
|
||||||
|
return False
|
||||||
from library.models import QuizRating
|
from library.models import QuizRating
|
||||||
try:
|
try:
|
||||||
game = QuizGame.objects.get(join_code=self.join_code)
|
game = QuizGame.objects.get(join_code=self.join_code)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ from django.utils import timezone
|
|||||||
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, HttpResponseNotFound
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -144,7 +145,8 @@ def finished(request, join_code):
|
|||||||
context = {
|
context = {
|
||||||
'join_code': join_code,
|
'join_code': join_code,
|
||||||
'is_host': str(game.host_id) == str(request.session.get('host_id')),
|
'is_host': str(game.host_id) == str(request.session.get('host_id')),
|
||||||
'participant_id': participant_id if participant else None
|
'participant_id': participant_id if participant else None,
|
||||||
|
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
|
||||||
}
|
}
|
||||||
return render(request, 'play/game/finished.html', context)
|
return render(request, 'play/game/finished.html', context)
|
||||||
except QuizGame.DoesNotExist:
|
except QuizGame.DoesNotExist:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<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">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
|
||||||
<title>qivip</title>
|
<title>qivip</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -193,24 +193,26 @@
|
|||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
{% if quiz.base_quiz %}
|
{% if quiz.base_quiz %}
|
||||||
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="font-bold"> {{ quiz.user_id}}</span></p>
|
<p class="text-gray-600 mt-2 ">Bearbeitung:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-gray-600 mt-2 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
{% if quiz.base_quiz %}
|
{% 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>
|
<p class="text-gray-600 ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="text-blue-600 font-bold">{{ quiz.base_quiz}}</span></a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
{% if quiz.base_quiz %}
|
{% if quiz.base_quiz %}
|
||||||
<p class="text-gray-600 ">Autor:<span class="font-bold"> {{ quiz.base_quiz.user_id}}</span></p>
|
<p class="text-gray-600 ">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
{% if quiz.credits %}
|
{% if quiz.credits %}
|
||||||
<p class="text-gray-600 mb-2">Credits:<span class="font-bold"> {{ quiz.credits }}</span></p>
|
<p class="text-gray-600 mb-2">Credits:<span class="text-blue-600 font-bold"> {{ quiz.credits }}</span></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,21 @@
|
|||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p }}
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
{% if form.instance.image %}
|
||||||
|
<img src="{{ form.instance.image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||||
|
<label for="reset_image">
|
||||||
|
<input type="checkbox" name="reset_image" id="reset_image">
|
||||||
|
Bild zurücksetzen
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<label class="font-bold" for="quiz_image">Bild:</label>
|
||||||
|
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="quiz_image" id="quiz_image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex justify-center space-x-3">
|
<div class="flex justify-center space-x-3">
|
||||||
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
|
<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">
|
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">
|
||||||
|
|||||||
@@ -55,15 +55,15 @@
|
|||||||
<input type="hidden" name="filter" value="1">
|
<input type="hidden" name="filter" value="1">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
|
<label class="block text-sm font-medium text-gray-700">minimale Anzahl an Fragen</label>
|
||||||
<input type="number" name="min_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_min_amout_questions">
|
<input type="number" name="min_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_min_amout_questions" value="{{ form.min_amout_questions.value|default_if_none:'' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
|
<label class="block text-sm font-medium text-gray-700">maximale Anzahl an Fragen</label>
|
||||||
<input type="number" name="max_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_max_amout_questions">
|
<input type="number" name="max_amout_questions" class="border-2 border-gray-300 rounded-lg p-2 w-full" min="1" id="id_max_amout_questions" value="{{ form.max_amout_questions.value|default_if_none:'' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
|
<label class="block text-sm font-medium text-gray-700">veröffentlicht von User</label>
|
||||||
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user">
|
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-3 flex justify-end">
|
<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">
|
<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">
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||||
<!-- Hintergrundbild -->
|
<!-- Hintergrundbild -->
|
||||||
<div class="absolute inset-0 bg-cover bg-center"
|
<div class="absolute inset-0 bg-cover bg-center"
|
||||||
style="background-image: url({{ quiz.image.url }});">
|
style="background-image: url({{ quiz.image.image.url }});">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
|
|
||||||
<!-- Rating -->
|
<!-- Rating -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
{% if quiz.rating_count > 0 %}
|
||||||
{% for i in '12345'|make_list %}
|
{% for i in '12345'|make_list %}
|
||||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||||
<span class="text-yellow-500 text-base">★</span>
|
<span class="text-yellow-500 text-base">★</span>
|
||||||
@@ -175,6 +176,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -284,7 +288,7 @@
|
|||||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||||
<!-- Hintergrundbild -->
|
<!-- Hintergrundbild -->
|
||||||
<div class="absolute inset-0 bg-cover bg-center"
|
<div class="absolute inset-0 bg-cover bg-center"
|
||||||
style="background-image: url({{ quiz.image.url }});">
|
style="background-image: url({{ quiz.image.image.url }});">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -355,6 +359,7 @@
|
|||||||
|
|
||||||
<!-- Rating -->
|
<!-- Rating -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
{% if quiz.rating_count > 0 %}
|
||||||
{% for i in '12345'|make_list %}
|
{% for i in '12345'|make_list %}
|
||||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||||
<span class="text-yellow-500 text-base">★</span>
|
<span class="text-yellow-500 text-base">★</span>
|
||||||
@@ -362,7 +367,10 @@
|
|||||||
<span class="text-gray-300 text-base">★</span>
|
<span class="text-gray-300 text-base">★</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -479,7 +487,7 @@
|
|||||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||||
<!-- Hintergrundbild -->
|
<!-- Hintergrundbild -->
|
||||||
<div class="absolute inset-0 bg-cover bg-center"
|
<div class="absolute inset-0 bg-cover bg-center"
|
||||||
style="background-image: url({{ quiz.image.url }});">
|
style="background-image: url({{ quiz.image.image.url }});">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -548,6 +556,7 @@
|
|||||||
|
|
||||||
<!-- Rating -->
|
<!-- Rating -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
{% if quiz.rating_count > 0 %}
|
||||||
{% for i in '12345'|make_list %}
|
{% for i in '12345'|make_list %}
|
||||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||||
<span class="text-yellow-500 text-base">★</span>
|
<span class="text-yellow-500 text-base">★</span>
|
||||||
@@ -556,6 +565,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% if image %}
|
{% if image %}
|
||||||
<img src="{{ image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
<img src="{{ image.image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
||||||
|
<label for="reset_image">
|
||||||
|
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||||
|
Bild zurücksetzen
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<label class="font-bold" for="question_image">Bild:</label>
|
<label class="font-bold" for="question_image">Bild:</label>
|
||||||
|
|||||||
@@ -25,7 +25,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if image %}
|
{% if image %}
|
||||||
<img src="{{ image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
<img src="{{ image.image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
||||||
|
<label for="reset_image">
|
||||||
|
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||||
|
Bild zurücksetzen
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<label class="font-bold" for="question_image">Bild:</label>
|
<label class="font-bold" for="question_image">Bild:</label>
|
||||||
|
|||||||
@@ -1,34 +1,121 @@
|
|||||||
<nav class="flex justify-between items-center bg-blue-600 h-12 px-4 sm:px-6 rounded-lg mt-1 m-2 shadow-md overflow-x-auto whitespace-nowrap">
|
<nav id="main-nav" class="relative bg-blue-600 rounded-lg mt-1 m-2 shadow-md transition-all duration-300">
|
||||||
<div class="flex items-center flex-shrink-0 md:w-32">
|
<!-- Desktop Navigation Bar -->
|
||||||
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200 ">
|
<div class="flex justify-between items-center h-12 px-4 sm:px-6 overflow-x-auto whitespace-nowrap">
|
||||||
<a href="{% url 'homepage:home' %}">qivip</a>
|
<!-- Logo / Left section -->
|
||||||
</h2>
|
<div class="flex items-center flex-shrink-0 md:w-32">
|
||||||
|
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200">
|
||||||
|
<a href="{% url 'homepage:home' %}">qivip</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Search (centered on screen) - Hidden on mobile -->
|
||||||
|
<div class="hidden md:flex justify-center items-center absolute left-1/2 transform -translate-x-1/2 min-w-40">
|
||||||
|
<form method="get" action="{% url 'library:overview_quiz' %}" class="mr-2 ml-2 flex items-center justify-center w-screen 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="text-sm w-full px-3 py-1 text-black bg-transparent focus:outline-none lg:text-base">
|
||||||
|
<button 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-5 md: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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation Links - Hidden on mobile -->
|
||||||
|
<div class="hidden md:flex items-center space-x-4 md:w-40">
|
||||||
|
<ul class="flex space-x-4 qp-nav-list">
|
||||||
|
{% if show_search %}
|
||||||
|
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
|
||||||
|
{% else%}
|
||||||
|
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li><a href="{% url 'accounts:home' %}">Konto</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'accounts:login' %}">Anmelden</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile menu button - Only visible on mobile -->
|
||||||
|
<div class="flex md:hidden items-center">
|
||||||
|
<button id="mobile-menu-button" class="text-white focus:outline-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center items-center min-w-40">
|
<!-- Mobile Navigation Menu - Hidden by default - FULLWIDTH DROPDOWN -->
|
||||||
<form method="get" action="{% url 'library:overview_quiz' %}" class="mr-2 ml-2 flex items-center justify-center w-screen max-w-sm bg-white rounded-full px-4 py-1 shadow-md">
|
<div id="mobile-menu" class="hidden absolute w-full z-50 md:hidden opacity-0 transform -translate-y-2 transition-all duration-300">
|
||||||
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
|
<div class="bg-blue-500 pt-3 pb-4 rounded-b-lg shadow-inner">
|
||||||
class=" text-sm w-full px-3 py-1 text-black bg-transparent focus:outline-none lg:text-base">
|
<!-- Search in mobile menu -->
|
||||||
<button class="py-1 text-blue-600">
|
<form method="get" action="{% url 'library:overview_quiz' %}" class="px-4 mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 md:size-6 ">
|
<div class="flex items-center justify-center bg-white rounded-full px-4 py-2 shadow-md">
|
||||||
<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" />
|
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
|
||||||
</svg>
|
class="w-full text-sm px-2 text-black bg-transparent focus:outline-none">
|
||||||
</button>
|
<button class="text-blue-600">
|
||||||
</form></div>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
stroke="currentColor" class="size-5">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 md:w-40 ">
|
<!-- Mobile Navigation Links -->
|
||||||
<ul class="flex space-x-4 qp-nav-list">
|
<div class="space-y-2 px-4">
|
||||||
{% if show_search %}
|
<a href="{% url 'library:overview_quiz' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
|
||||||
<li class="hidden md:flex"><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
|
Bibliothek
|
||||||
{% else%}
|
</a>
|
||||||
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
|
{% if user.is_authenticated %}
|
||||||
{% endif %}
|
<a href="{% url 'accounts:home' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
|
||||||
{% if user.is_authenticated %}
|
Konto
|
||||||
<li><a href="{% url 'accounts:home' %}">Konto</a></li>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{% url 'accounts:login' %}">Anmelden</a></li>
|
<a href="{% url 'accounts:login' %}" class="block py-2 text-white font-medium rounded-lg hover:bg-blue-600 transition duration-200">
|
||||||
{% endif %}
|
Anmelden
|
||||||
</ul>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Script -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
const mainNav = document.getElementById('main-nav');
|
||||||
|
|
||||||
|
mobileMenuButton.addEventListener('click', function() {
|
||||||
|
// Toggle menu visibility
|
||||||
|
if (mobileMenu.classList.contains('hidden')) {
|
||||||
|
// Show menu
|
||||||
|
mobileMenu.classList.remove('hidden');
|
||||||
|
// Entferne unten abgerundete Ecken der Navbar wenn Menü offen
|
||||||
|
mainNav.classList.remove('rounded-lg');
|
||||||
|
mainNav.classList.add('rounded-t-lg');
|
||||||
|
// Animation einfahren
|
||||||
|
setTimeout(() => {
|
||||||
|
mobileMenu.classList.remove('opacity-0', 'transform', '-translate-y-2');
|
||||||
|
mobileMenu.classList.add('opacity-100');
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
// Ausfahren Animation
|
||||||
|
mobileMenu.classList.add('opacity-0', 'transform', '-translate-y-2');
|
||||||
|
mobileMenu.classList.remove('opacity-100');
|
||||||
|
// Nach der Animation verstecken
|
||||||
|
setTimeout(() => {
|
||||||
|
mobileMenu.classList.add('hidden');
|
||||||
|
// Stelle die abgerundeten Ecken wieder her
|
||||||
|
mainNav.classList.remove('rounded-t-lg');
|
||||||
|
mainNav.classList.add('rounded-lg');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not is_host %}
|
{% if not is_host %}
|
||||||
|
{% if rating_enabled %}
|
||||||
<div class="bg-blue-50 rounded-lg p-6 mb-8">
|
<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>
|
<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">
|
<div class="stars flex justify-center space-x-4 mb-4">
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p id="rating-message" class="text-center text-blue-600 font-medium"></p>
|
<p id="rating-message" class="text-center text-blue-600 font-medium"></p>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center text-blue-600 font-medium mb-8">Bewertungen sind deaktiviert</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
{% if question_image %}
|
{% if question_image %}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<img class="m-4" src="{{ question_image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
<img class="m-4" src="{{ question_image.image.url }}" style="max-width: 500px;" alt="Bild der Frage">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -17,52 +17,109 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.session.mode != "learn" %}
|
{% if request.session.mode != "learn" %}
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
|
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
|
||||||
<div class="space-y-2">
|
<div id="scoreboard" class="space-y-2">
|
||||||
{% for participant in participants %}
|
{% for participant in participants %}
|
||||||
{% if participant == my_participant or is_host %}
|
{% if participant == my_participant or is_host %}
|
||||||
|
<div class="p-4 bg-gray-100 rounded-lg flex justify-between items-center border {% if forloop.counter <= 3 %}border-yellow-500{% else %}border-transparent{% endif %}">
|
||||||
<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>
|
||||||
<div>
|
<p class="text-lg font-bold">
|
||||||
<p class="text-lg font-bold">{{ participant.display_name }}</p>
|
{% if forloop.counter == 1 %}
|
||||||
<p class="text-gray-600">{{ participant.score }} Punkte</p>
|
🥇
|
||||||
</div>
|
{% elif forloop.counter == 2 %}
|
||||||
{% if participant.last_answer_correct %}
|
🥈
|
||||||
<span class="text-green-500">✓</span>
|
{% elif forloop.counter == 3 %}
|
||||||
{% elif participant.last_answer_correct == False %}
|
🥉
|
||||||
<span class="text-red-500">✗</span>
|
{% endif %}
|
||||||
{% endif %}
|
{{ participant.display_name }}
|
||||||
</div>
|
</p>
|
||||||
{% else %}
|
<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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% 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 %}
|
||||||
{% if is_host %}
|
{% endif %}
|
||||||
{% 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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
{% if is_last_question %}
|
||||||
|
<script>
|
||||||
|
// Hilfsfunktion für Pausen
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const items = document.querySelectorAll('#scoreboard div');
|
||||||
|
const top3 = Array.from(items).slice(0, 3); // Nur die ersten 3 animieren
|
||||||
|
|
||||||
|
// Sofort Startzustände für Top3 setzen, die anderen sofort sichtbar
|
||||||
|
items.forEach((el, index) => {
|
||||||
|
if (index >= 3) {
|
||||||
|
el.style.opacity = '1';
|
||||||
|
el.style.transform = 'translateY(0)';
|
||||||
|
} else {
|
||||||
|
el.style.opacity = '0';
|
||||||
|
el.style.transform = 'translateY(20px)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warten bevor Animation startet
|
||||||
|
await sleep(1500); // <<< hier die 5 Sekunden Pause
|
||||||
|
|
||||||
|
// Transition-Eigenschaft setzen nach Render-Zyklus
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
top3.forEach(el => {
|
||||||
|
el.style.transition = 'all 0.8s ease';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation für Top 3 starten – von Platz 3 zu 1
|
||||||
|
top3.reverse().forEach((el, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
el.style.opacity = '1';
|
||||||
|
el.style.transform = 'translateY(0)';
|
||||||
|
|
||||||
|
if (index === top3.length - 1) {
|
||||||
|
triggerConfetti();
|
||||||
|
}
|
||||||
|
}, index * 1500); // Zeitversetzt aufdecken
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function triggerConfetti() {
|
||||||
|
const end = Date.now() + 15 * 1000;
|
||||||
|
(function frame() {
|
||||||
|
confetti({ particleCount: 2, angle: 60, spread: 55, origin: { x: 0 } });
|
||||||
|
confetti({ particleCount: 2, angle: 120, spread: 55, origin: { x: 1 } });
|
||||||
|
if (Date.now() < end) requestAnimationFrame(frame);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% include 'play/game/common_scripts.html' %}
|
{% include 'play/game/common_scripts.html' %}
|
||||||
<script>
|
<script>
|
||||||
const joinCode = '{{ quiz_game.join_code }}';
|
const joinCode = '{{ quiz_game.join_code }}';
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ channels~=4.2.0 # WebSocket support
|
|||||||
daphne~=4.1.2 # ASGI server
|
daphne~=4.1.2 # ASGI server
|
||||||
pillow~=11.1.0 # Image handling
|
pillow~=11.1.0 # Image handling
|
||||||
whitenoise~=6.6.0 # Static file serving
|
whitenoise~=6.6.0 # Static file serving
|
||||||
django-cleanup
|
django-cleanup
|
||||||
|
redis
|
||||||
|
channels_redis
|
||||||
Reference in New Issue
Block a user