Compare commits
1 Commits
master
...
128-testbr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e84c13c40a |
@@ -170,12 +170,3 @@ Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
|
||||
| Variable | Bedeutung | Optionen |
|
||||
| --- | --- | --- |
|
||||
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
|
||||
|
||||
|
||||
## Lizenzen für node_modules
|
||||
1. Installation: npm install license-checker --save-dev
|
||||
|
||||
npx license-checker --markdown > THIRD_PARTY_LICENSES.md
|
||||
## für Python Packete:
|
||||
|
||||
pip-licenses --format=markdown > THIRD_PARTY_LICENSES_PYTHON.md
|
||||
@@ -1,45 +0,0 @@
|
||||
| Name | Version | License |
|
||||
|---------------------------|--------------|---------------------------------------------------------|
|
||||
| Automat | 25.4.16 | MIT License |
|
||||
| Django | 5.1.13 | BSD License |
|
||||
| Twisted | 25.5.0 | MIT License |
|
||||
| asgiref | 3.10.0 | BSD License |
|
||||
| attrs | 25.4.0 | UNKNOWN |
|
||||
| autobahn | 25.10.1 | UNKNOWN |
|
||||
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
|
||||
| cffi | 2.0.0 | UNKNOWN |
|
||||
| channels | 4.2.2 | BSD License |
|
||||
| channels_redis | 4.3.0 | BSD |
|
||||
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
|
||||
| charset-normalizer | 3.4.4 | MIT |
|
||||
| colorama | 0.4.6 | BSD License |
|
||||
| constantly | 23.10.4 | MIT License |
|
||||
| cryptography | 46.0.3 | UNKNOWN |
|
||||
| daphne | 4.1.2 | BSD License |
|
||||
| django-autocomplete-light | 3.12.1 | MIT License |
|
||||
| django-cleanup | 9.0.0 | MIT License |
|
||||
| geographiclib | 2.1 | MIT |
|
||||
| geopy | 2.4.1 | MIT License |
|
||||
| hyperlink | 21.0.0 | MIT License |
|
||||
| idna | 3.11 | UNKNOWN |
|
||||
| incremental | 24.7.2 | MIT License |
|
||||
| msgpack | 1.1.2 | UNKNOWN |
|
||||
| pillow | 11.1.0 | CMU License (MIT-CMU) |
|
||||
| pyOpenSSL | 25.3.0 | Apache Software License |
|
||||
| pyasn1 | 0.6.1 | BSD License |
|
||||
| pyasn1_modules | 0.4.2 | BSD License |
|
||||
| pycparser | 2.23 | BSD License |
|
||||
| pypng | 0.20220715.0 | MIT License |
|
||||
| python-dotenv | 1.1.1 | BSD License |
|
||||
| qrcode | 7.4.2 | BSD License |
|
||||
| redis | 6.4.0 | MIT License |
|
||||
| reportlab | 4.2.5 | BSD License |
|
||||
| requests | 2.32.5 | Apache Software License |
|
||||
| service-identity | 24.2.0 | MIT License |
|
||||
| sqlparse | 0.5.3 | BSD License |
|
||||
| txaio | 25.9.2 | UNKNOWN |
|
||||
| typing_extensions | 4.15.0 | UNKNOWN |
|
||||
| tzdata | 2025.2 | Apache Software License |
|
||||
| urllib3 | 2.5.0 | UNKNOWN |
|
||||
| whitenoise | 6.6.0 | MIT License |
|
||||
| zope.interface | 8.0.1 | Zope Public License |
|
||||
Binary file not shown.
@@ -6,25 +6,13 @@ from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import logout
|
||||
from library.models import QivipQuiz, QivipQuestion
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
|
||||
# Create your views here.
|
||||
@login_required
|
||||
def home(request):
|
||||
own_critisized_quizzes = QivipQuiz.objects.annotate(
|
||||
criticism_count=Count('criticism_quiz')
|
||||
).filter(criticism_count__gt=0).filter(user_id=request.user)
|
||||
|
||||
|
||||
my_quizzes_number=QivipQuiz.objects.filter(user_id=request.user).count()
|
||||
my_questions_number = QivipQuestion.objects.filter(quiz_id__user_id=request.user).count()
|
||||
context = {
|
||||
'own_critisized_quizzes':own_critisized_quizzes,
|
||||
'my_quizzes_number': my_quizzes_number,
|
||||
'my_questions_number': my_questions_number,
|
||||
}
|
||||
return render(request, 'accounts/home.html', context=context)
|
||||
return render(request, 'accounts/home.html')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,9 +31,8 @@ load_dotenv(env_path)
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-&psk#na5l=p3q8_a+-$4w1f^lt3lx1c@d*p4x$ymm_rn7pwb87')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
#DEBUG = False
|
||||
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
|
||||
#DEBUG = True
|
||||
DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
|
||||
|
||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
|
||||
|
||||
@@ -56,9 +55,6 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'channels',
|
||||
'django_cleanup',
|
||||
'django.contrib.humanize',
|
||||
'dal',
|
||||
'dal_select2',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -97,7 +93,7 @@ CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [("127.0.0.1", 6380)],
|
||||
"hosts": [("127.0.0.1", 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -178,13 +174,3 @@ import os
|
||||
|
||||
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien
|
||||
|
||||
# Email Settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'postoffice.katharineum.de'
|
||||
EMAIL_PORT = 587 # 25
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = 'qivip-mail@katharineum.de'
|
||||
EMAIL_HOST_PASSWORD = '<password>'
|
||||
EMAIL_USE_SSL = False
|
||||
DEFAULT_FROM_EMAIL = 'qivip-mail@katharineum.de'
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
from django.contrib import admin
|
||||
from .models import QivipFaq, QivipDescription
|
||||
|
||||
# Register your models here.
|
||||
class QivipFaqAdmin(admin.ModelAdmin):
|
||||
list_display = ('question','answer',)
|
||||
|
||||
class QivipDescriptionAdmin(admin.ModelAdmin):
|
||||
list_display = ('title','description',)
|
||||
|
||||
# Registrierung der Modelle im Admin
|
||||
admin.site.register(QivipFaq, QivipFaqAdmin)
|
||||
admin.site.register(QivipDescription, QivipDescriptionAdmin)
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-01 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QivipFAQs',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('question', models.CharField(max_length=200)),
|
||||
('answer', models.CharField(max_length=1000)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-01 17:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='QivipFAQs',
|
||||
new_name='QivipFaq',
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-01 17:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0002_rename_qivipfaqs_qivipfaq'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='qivipfaq',
|
||||
name='answer',
|
||||
field=models.TextField(max_length=1000),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='qivipfaq',
|
||||
name='question',
|
||||
field=models.TextField(max_length=200),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 17:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0003_alter_qivipfaq_answer_alter_qivipfaq_question'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QivipInfos',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.TextField(max_length=200)),
|
||||
('description', models.TextField(max_length=1000)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 17:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0004_qivipinfos'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='QivipInfos',
|
||||
new_name='QivipInfo',
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 17:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0005_rename_qivipinfos_qivipinfo'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QivipDescription',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.TextField(help_text='Funktion von qivip', max_length=200)),
|
||||
('description', models.TextField(help_text='Beschreibung derFunktion von qivip', max_length=1000)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='QivipInfo',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 17:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('homepage', '0006_qivipdescription_delete_qivipinfo'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='qivipdescription',
|
||||
name='description',
|
||||
field=models.TextField(help_text='Beschreibung der Funktion von qivip', max_length=1000),
|
||||
),
|
||||
]
|
||||
@@ -1,10 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class QivipFaq(models.Model):
|
||||
question = models.TextField(max_length=200)
|
||||
answer = models.TextField(max_length=1000)
|
||||
|
||||
class QivipDescription(models.Model): # Was kann man mit qivip machen ...
|
||||
title = models.TextField(max_length=200, help_text="Funktion von qivip")
|
||||
description= models.TextField(max_length=1000, help_text="Beschreibung der Funktion von qivip")
|
||||
@@ -1,11 +1,7 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from .models import QivipFaq, QivipDescription
|
||||
|
||||
def home(request):
|
||||
faqs=QivipFaq.objects.all()
|
||||
descriptions=QivipDescription.objects.all()
|
||||
context={"faqs": faqs, "descriptions": descriptions}
|
||||
return render(request, 'homepage/home.html', context)
|
||||
return render(request, 'homepage/home.html')
|
||||
|
||||
def impress(request):
|
||||
return render(request, 'homepage/impress.html')
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
from django.contrib import admin
|
||||
from .models import QivipQuiz, QivipQuestion,QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating, QuizCriticism
|
||||
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage
|
||||
|
||||
# Für das QivipQuiz Modell
|
||||
class QivipQuizAdmin(admin.ModelAdmin):
|
||||
list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
|
||||
list_display = ('name', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
|
||||
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
|
||||
list_filter = ('status', 'category') # Filteroptionen
|
||||
autocomplete_fields = ("reference_quizzes",)
|
||||
|
||||
|
||||
class QuizCriticismAdmin(admin.ModelAdmin):
|
||||
list_display = ('quiz','question_criticism', 'user','criticism') # Welche Felder sollen in der Übersicht angezeigt werden
|
||||
search_fields = ('quiz','question_criticism', 'user','criticism') # Suchfelder
|
||||
list_filter=('quiz','question_criticism', 'user','criticism')
|
||||
|
||||
|
||||
|
||||
|
||||
# Für das QivipQuestion Modell
|
||||
class QivipQuestionAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')
|
||||
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date')
|
||||
search_fields = ('data',)
|
||||
list_filter = ('quiz_id',)
|
||||
|
||||
@@ -41,35 +30,10 @@ class QuizImageAdmin(admin.ModelAdmin):
|
||||
class QuestionImageAdmin(admin.ModelAdmin):
|
||||
list_display = ('image',)
|
||||
|
||||
|
||||
from django import forms
|
||||
from .models import QuizCategory
|
||||
|
||||
class QuizCategoryForm(forms.ModelForm):
|
||||
color = forms.CharField(
|
||||
widget=forms.TextInput(attrs={'type': 'color'})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = QuizCategory
|
||||
fields = '__all__'
|
||||
from django.utils.html import format_html
|
||||
@admin.register(QuizCategory)
|
||||
class QuizCategoryAdmin(admin.ModelAdmin):
|
||||
form = QuizCategoryForm
|
||||
list_display = ('name', 'color_preview', 'color')
|
||||
|
||||
def color_preview(self, obj):
|
||||
return format_html(
|
||||
'<div style="width: 24px; height: 24px; background-color: {}; border: 2px solid #000;"></div>',
|
||||
obj.color
|
||||
)
|
||||
color_preview.short_description = "Farbe"
|
||||
|
||||
# Registrierung der Modelle im Admin
|
||||
admin.site.register(QivipQuiz, QivipQuizAdmin)
|
||||
admin.site.register(QivipQuestion, QivipQuestionAdmin)
|
||||
admin.site.register(QuizCriticism, QuizCriticismAdmin)
|
||||
admin.site.register(QuizCategory, QuizCategoryAdmin)
|
||||
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin)
|
||||
admin.site.register(QuizImage, QuizImageAdmin)
|
||||
admin.site.register(QuestionImage, QuestionImageAdmin)
|
||||
@@ -1,33 +1,10 @@
|
||||
from django import forms
|
||||
from .models import QivipQuiz, QivipQuestion, QuizCategory
|
||||
from dal import autocomplete
|
||||
from .models import QivipQuiz, QivipQuestion
|
||||
|
||||
class QuizForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = QivipQuiz
|
||||
fields = ['name', 'description', 'status', 'category','difficulty','reference_quizzes','credits']
|
||||
widgets = {
|
||||
'reference_quizzes': autocomplete.ModelSelect2Multiple(
|
||||
url='library:quiz-autocomplete',
|
||||
forward=['pk']
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(QuizForm, self).__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({
|
||||
|
||||
})
|
||||
self.fields["reference_quizzes"].queryset = QivipQuiz.objects.filter(
|
||||
status="öffentlich").exclude(pk=self.instance.pk)
|
||||
self.fields["reference_quizzes"].label_from_instance = lambda obj: f"{obj.name} (ID: {obj.pk}) (User: {obj.user_id})"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fields = ['name', 'description', 'status', 'category','difficulty','credits']
|
||||
|
||||
class QuestionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -42,20 +19,13 @@ class QuizFilterForm(forms.Form):
|
||||
}))
|
||||
|
||||
|
||||
min_amount_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
|
||||
min_amout_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
|
||||
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
|
||||
}))
|
||||
|
||||
max_amount_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
|
||||
max_amout_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
|
||||
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
|
||||
}))
|
||||
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
|
||||
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
|
||||
}))
|
||||
categories = forms.ModelChoiceField(
|
||||
queryset=QuizCategory.objects.filter().distinct(),
|
||||
required=False,
|
||||
label="Kategorie",
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full dark:bg-[#2a2f3a] dark:text-white'
|
||||
}))
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-13 16:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0053_questionimage_quizimage_alter_qivipquestion_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='qivipquestion',
|
||||
name='credits',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-26 16:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0054_qivipquestion_credits'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='published_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-06-27 12:35
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0055_qivipquiz_published_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='quizrating',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-02 11:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0056_alter_quizrating_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizcategory',
|
||||
name='color',
|
||||
field=models.CharField(default='#000000', help_text='Farbe als Hex-Code, z.B. #ff0000', max_length=7),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 14:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0057_quizcategory_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='quiz_played_Lernmodus',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='quiz_played_Moderiermodus',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='quiz_played_Moderiermodus_players',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='single_player_or_multiple',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 15:04
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0058_qivipquiz_quiz_played_lernmodus_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='qivipquiz',
|
||||
name='single_player_or_multiple',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 18:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0059_remove_qivipquiz_single_player_or_multiple'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='qivipquiz',
|
||||
name='similar_quizzes',
|
||||
field=models.ManyToManyField(blank=True, related_name='similar_to', to='library.qivipquiz'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 18:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0060_qivipquiz_similar_quizzes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='qivipquiz',
|
||||
name='similar_quizzes',
|
||||
field=models.ManyToManyField(blank=True, to='library.qivipquiz'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-27 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0061_alter_qivipquiz_similar_quizzes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='qivipquiz',
|
||||
name='similar_quizzes',
|
||||
field=models.ManyToManyField(blank=True, to='library.qivipquiz', verbose_name='Verweis auf andere Quizze'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-03 11:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0062_alter_qivipquiz_similar_quizzes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='qivipquiz',
|
||||
old_name='similar_quizzes',
|
||||
new_name='reference_quizzes',
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-30 13:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0063_rename_similar_quizzes_qivipquiz_reference_quizzes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QuizCriticism',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('criticism', models.CharField(max_length=256, verbose_name='Kritik')),
|
||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criticism_question', to='library.qivipquestion')),
|
||||
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criticism_quiz', to='library.qivipquiz')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('quiz', 'user', 'question')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-30 13:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0064_quizcriticism'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizcriticism',
|
||||
name='creation_date',
|
||||
field=models.DateTimeField(auto_now_add=True, default='2025-12-30 12:00:00'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-30 13:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0065_quizcriticism_creation_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='quizcriticism',
|
||||
name='creation_date',
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-30 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0066_remove_quizcriticism_creation_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizcriticism',
|
||||
name='creation_date',
|
||||
field=models.DateTimeField(auto_now_add=True, default='2025-12-30 12:00:00'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-31 11:56
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0067_quizcriticism_creation_date'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='quizcriticism',
|
||||
old_name='question',
|
||||
new_name='question_criticism',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='quizcriticism',
|
||||
unique_together={('quiz', 'user', 'question_criticism', 'criticism')},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-31 12:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0068_rename_question_quizcriticism_question_criticism_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='quizcriticism',
|
||||
name='question_criticism',
|
||||
field=models.CharField(max_length=1024),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-31 14:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0069_alter_quizcriticism_question_criticism'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizcriticism',
|
||||
name='question',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='library.qivipquestion'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-12-31 14:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0070_quizcriticism_question'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizcriticism',
|
||||
name='id_question',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2026-01-17 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0071_quizcriticism_id_question'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='qivipquestion',
|
||||
options={'ordering': ['order']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='qivipquestion',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
|
||||
from django.utils.text import slugify
|
||||
import uuid
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class QuizImage(models.Model):
|
||||
image = models.ImageField(max_length=256,verbose_name="Bild", upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
|
||||
@@ -43,20 +43,11 @@ class QivipQuiz(models.Model):
|
||||
on_delete=models.PROTECT,
|
||||
related_name="quizzes"
|
||||
)
|
||||
reference_quizzes = models.ManyToManyField(
|
||||
'self',
|
||||
blank=True,
|
||||
symmetrical=False,
|
||||
verbose_name="Verweis auf andere Quizze"
|
||||
)
|
||||
|
||||
|
||||
quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0)
|
||||
quiz_played_Moderiermodus= models.IntegerField(null=True, blank=True, default=0)
|
||||
quiz_played_Moderiermodus_players= models.IntegerField(null=True, blank=True, default=0)
|
||||
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
|
||||
#creator = models.ForeignKey(User, on_delete=models.SET_NULL,blank=True, null=True, related_name='creator_quiz')
|
||||
base_quiz = models.ForeignKey('self', on_delete=models.SET_NULL,blank=True, null=True, related_name='child_quizzes')
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
@@ -68,10 +59,6 @@ class QivipQuiz(models.Model):
|
||||
credits=models.TextField(blank=True, editable=True)
|
||||
average_rating = models.FloatField(default=3.0)
|
||||
rating_count = models.IntegerField(default=0)
|
||||
published_at = models.DateTimeField(null=True, blank=True) # manuell gesetzt, wenn veröffentlicht
|
||||
|
||||
def is_published(self):
|
||||
return self.published_at is not None and self.published_at <= timezone.now()
|
||||
def __str__(self):
|
||||
|
||||
return self.name
|
||||
@@ -86,7 +73,6 @@ class QivipQuizFavorite(models.Model):
|
||||
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
|
||||
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class QivipQuestion(models.Model):
|
||||
|
||||
@@ -112,30 +98,10 @@ class QivipQuestion(models.Model):
|
||||
on_delete=models.PROTECT,
|
||||
related_name="questions"
|
||||
)
|
||||
credits=models.TextField(blank=True, editable=True)
|
||||
order = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order'] # Fragen nach Reihenfolge sortieren
|
||||
|
||||
def __str__(self):
|
||||
return self.data[:50]
|
||||
|
||||
class QuizCriticism(models.Model):
|
||||
quiz= models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='criticism_quiz')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
question_criticism=models.CharField(max_length=1024)
|
||||
criticism=models.CharField(max_length=256, verbose_name="Kritik")
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
question = models.ForeignKey(QivipQuestion, on_delete=models.CASCADE,null=True, blank=True)
|
||||
id_question=models.IntegerField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('quiz', 'user', 'question_criticism','criticism')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.question_criticism}"
|
||||
|
||||
|
||||
|
||||
class QuizRating(models.Model):
|
||||
@@ -144,22 +110,9 @@ class QuizRating(models.Model):
|
||||
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
#class Meta:
|
||||
#unique_together = ('quiz', 'participant_id')
|
||||
class Meta:
|
||||
unique_together = ('quiz', 'participant_id')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update quiz average rating (immer, auch bei Updates)
|
||||
quiz = self.quiz
|
||||
total_ratings = quiz.ratings.count()
|
||||
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
|
||||
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
|
||||
quiz.rating_count = total_ratings
|
||||
quiz.save()
|
||||
|
||||
|
||||
"""
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
@@ -171,18 +124,12 @@ class QuizRating(models.Model):
|
||||
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
|
||||
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
|
||||
quiz.rating_count = total_ratings
|
||||
quiz.save()"""
|
||||
quiz.save()
|
||||
|
||||
class QuizCategory(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
color= models.CharField(
|
||||
max_length=7,
|
||||
default="#000000",
|
||||
help_text="Farbe als Hex-Code, z.B. #ff0000"
|
||||
)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
|
||||
@@ -3,13 +3,10 @@ from django.urls import path
|
||||
from django.conf import settings
|
||||
from . import views
|
||||
from django.conf.urls.static import static
|
||||
from .views import QuizAutocomplete
|
||||
app_name = 'library'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.overview_quiz, name='overview_quiz'),
|
||||
path('my/', views.overview_quiz_my, name='overview_quiz_my'),
|
||||
path('favorites/', views.overview_quiz_favorites, name='overview_quiz_favorites'),
|
||||
path('new/', views.new_quiz, name='new_quiz'),
|
||||
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
|
||||
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
|
||||
@@ -19,16 +16,6 @@ urlpatterns = [
|
||||
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
|
||||
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
|
||||
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
|
||||
path('critique/delete/<int:critique_id>/<int:quiz_id>', views.delete_critique, name='delete_critique'),
|
||||
path('quiz/reorder/', views.reorder_questions, name='reorder_questions'),
|
||||
|
||||
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
|
||||
path(
|
||||
"autocomplete/quiz",
|
||||
QuizAutocomplete.as_view(),
|
||||
name="quiz-autocomplete"
|
||||
),
|
||||
|
||||
|
||||
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCriticism, QuizImage
|
||||
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizImage
|
||||
from .forms import QuizForm, QuestionForm
|
||||
import json
|
||||
from django.core.paginator import Paginator
|
||||
@@ -14,64 +14,8 @@ from .models import QivipQuiz
|
||||
from .forms import QuizFilterForm
|
||||
from django.db.models import Count
|
||||
from django.contrib.auth.models import User
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
# Übersicht aller Quizze
|
||||
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.lib import colors
|
||||
from django.http import HttpResponse
|
||||
import json
|
||||
from datetime import datetime
|
||||
from reportlab.platypus import Image
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
import json
|
||||
from io import BytesIO
|
||||
import requests
|
||||
from django.core.files.storage import default_storage
|
||||
from PIL import Image as PILImage, ImageOps
|
||||
from django.conf import settings
|
||||
import os
|
||||
from django.utils.html import escape
|
||||
from .models import QivipQuiz
|
||||
from django.db.models import Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
# views.py
|
||||
from dal import autocomplete
|
||||
from .models import QivipQuiz
|
||||
|
||||
class QuizAutocomplete(autocomplete.Select2QuerySetView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = QivipQuiz.objects.filter(status="öffentlich")
|
||||
|
||||
pk = self.forwarded.get('pk', None)
|
||||
if pk:
|
||||
qs = qs.exclude(pk=pk)
|
||||
|
||||
if self.q:
|
||||
qs = qs.annotate(
|
||||
search_text=Concat(
|
||||
'name',
|
||||
Value(' (ID: '), 'pk', Value(') (User: '), 'user_id', Value(')'),
|
||||
output_field=CharField()
|
||||
)
|
||||
).filter(search_text__icontains=self.q)
|
||||
return qs
|
||||
|
||||
def get_result_label(self, result):
|
||||
# z.B. Name + ID + User
|
||||
return f"{result.name} (ID: {result.pk}) (User: {result.user_id})"
|
||||
|
||||
|
||||
|
||||
|
||||
def quiz_names_json(request):
|
||||
form = QuizFilterForm(request.GET)
|
||||
@@ -79,12 +23,13 @@ def quiz_names_json(request):
|
||||
search = form.cleaned_data.get('search')
|
||||
|
||||
|
||||
names = QivipQuiz.objects.all().annotate(max_amount_questions=Count('questions')).filter(max_amount_questions__gte=1, status='öffentlich', name__icontains=search)
|
||||
names = QivipQuiz.objects.all().annotate(max_amout_questions=Count('questions')).filter(max_amout_questions__gte=1, status='öffentlich', name__icontains=search)
|
||||
|
||||
|
||||
if request.user.is_authenticated:
|
||||
names = names | QivipQuiz.objects.filter(
|
||||
user_id=request.user.id,
|
||||
status='privat',
|
||||
name__icontains=search
|
||||
)
|
||||
|
||||
@@ -93,256 +38,121 @@ def quiz_names_json(request):
|
||||
|
||||
return JsonResponse(names, safe=False)
|
||||
|
||||
def overview_quiz(request):
|
||||
# Filter
|
||||
form = QuizFilterForm(request.GET)
|
||||
try:
|
||||
favorite_quizzes_data = QivipQuizFavorite.objects.filter(user=request.user, favorite=True).order_by("-favorite_date")
|
||||
favorite_quiz_ids = favorite_quizzes_data.values_list('quiz', flat=True)
|
||||
favorite_quizzes = QivipQuiz.objects.filter(id__in=favorite_quiz_ids)
|
||||
except:
|
||||
favorite_quizzes_data = QivipQuizFavorite.objects.none()
|
||||
favorite_quizzes = QivipQuizFavorite.objects.none()
|
||||
favorite_quiz_ids = favorite_quizzes.none()
|
||||
favorite_quizzes = QivipQuiz.objects.none()
|
||||
|
||||
# Initialize querysets
|
||||
if request.user.is_authenticated:
|
||||
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions'))
|
||||
favorite_quizzes = favorite_quizzes.filter(id__in=favorite_quiz_ids).annotate(max_amout_questions=Count('questions'))
|
||||
filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
|
||||
else:
|
||||
favorite_quizzes = QivipQuiz.objects.none()
|
||||
filter_my_quizzes = QivipQuiz.objects.none()
|
||||
filter_other_quizzes = QivipQuiz.objects.all()
|
||||
|
||||
def reorder_questions(request):
|
||||
if request.method == "POST":
|
||||
data = json.loads(request.body)
|
||||
for item in data['order']:
|
||||
QivipQuestion.objects.filter(id=item['id']).update(order=item['position'])
|
||||
return JsonResponse({'status': 'ok'})
|
||||
return JsonResponse({'status': 'error'}, status=400)
|
||||
# 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')
|
||||
|
||||
|
||||
|
||||
def apply_quiz_filters(queryset, form):
|
||||
|
||||
if not form.is_valid():
|
||||
return queryset
|
||||
queryset = queryset.annotate(question_count=Count('questions'))
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
search = form.cleaned_data.get('search')
|
||||
username= form.cleaned_data.get('user')
|
||||
min_questions = form.cleaned_data.get('min_amount_questions')
|
||||
max_questions = form.cleaned_data.get('max_amount_questions')
|
||||
category = form.cleaned_data.get('categories')
|
||||
max_amout_questions= form.cleaned_data.get('max_amout_questions')
|
||||
min_amout_questions= form.cleaned_data.get('min_amout_questions')
|
||||
|
||||
filters = {}
|
||||
|
||||
if search:
|
||||
filters['name__icontains'] = search
|
||||
|
||||
if min_questions is not None:
|
||||
filters['question_count__gte'] = min_questions
|
||||
|
||||
if max_questions is not None:
|
||||
filters['question_count__lte'] = max_questions
|
||||
|
||||
|
||||
if category is not None:
|
||||
filters['category'] = category
|
||||
|
||||
|
||||
if username:
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
filters['user_id'] = user.id
|
||||
except User.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
return queryset.filter(**filters)
|
||||
|
||||
|
||||
|
||||
def critisize_quiz(request, pk):
|
||||
|
||||
quiz= get_object_or_404(QivipQuiz, pk=pk)
|
||||
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
question_id = request.POST.get("question_id") or "whole_quiz"
|
||||
|
||||
if question_id!="whole_quiz":
|
||||
question = quiz.questions.get(id=question_id)
|
||||
data = json.loads(question.data)
|
||||
question_text = data["question"]
|
||||
|
||||
else:
|
||||
question_text="Quiz(formular) kritisiert"
|
||||
question = None
|
||||
question_id = None
|
||||
|
||||
|
||||
critic_type = request.POST.get("critic")
|
||||
CRITICISM_MAP = {
|
||||
"content_error": "inhaltlicher Fehler",
|
||||
"spelling_error": "Rechtschreibfehler oder Grammatikfehler",
|
||||
"logic_error": "umständliche oder unsinnige Formulierung",
|
||||
"inappropriate_error": "unangemessen",
|
||||
"not_completed_error": "unvollständig",
|
||||
"source_error": "Quellen unvollständig",
|
||||
"quiz_error": "Quiz als \"öffentliches Quiz\" ungeeignet"}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exists = QuizCriticism.objects.filter(
|
||||
id_question=question_id,
|
||||
question=question,
|
||||
quiz=quiz,
|
||||
user=request.user,
|
||||
question_criticism=question_text,
|
||||
criticism=CRITICISM_MAP[critic_type],
|
||||
).exists()
|
||||
|
||||
if exists:
|
||||
messages.error(request, "Diese Kritik wurde bereits abgegeben.") # nur Kritik abgeben, wenn noch nicht erstellt
|
||||
else:
|
||||
QuizCriticism.objects.create(
|
||||
question=question,
|
||||
id_question=question_id,
|
||||
quiz=quiz,
|
||||
user=request.user,
|
||||
question_criticism=question_text,
|
||||
criticism=CRITICISM_MAP[critic_type],
|
||||
)
|
||||
|
||||
|
||||
|
||||
def delete_critique(request,critique_id,quiz_id):
|
||||
|
||||
critique = get_object_or_404(QuizCriticism, id=critique_id)
|
||||
critique.delete()
|
||||
return redirect('library:detail_quiz', pk=quiz_id)
|
||||
|
||||
|
||||
|
||||
|
||||
def get_critisized_quizzes():
|
||||
critisized_quizzes = QivipQuiz.objects.annotate(
|
||||
criticism_count=Count('criticism_quiz')
|
||||
).filter(criticism_count__gt=0)
|
||||
return critisized_quizzes
|
||||
|
||||
|
||||
def overview_quiz(request, connect="all"):
|
||||
|
||||
critisized_quizzes = get_critisized_quizzes()
|
||||
form = QuizFilterForm(request.GET)
|
||||
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
|
||||
# Meine Quizzeze mit mind. 1 Frage
|
||||
filter_my_quizzes = QivipQuiz.objects.none()
|
||||
if user:
|
||||
filter_my_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
|
||||
user_id=user.id,
|
||||
max_amount_questions__gte=0
|
||||
)
|
||||
|
||||
# Öffentliche Alle mit mind. 1 Frage
|
||||
filter_all_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
|
||||
status='öffentlich',
|
||||
max_amount_questions__gte=1
|
||||
)
|
||||
if user:
|
||||
filter_all_quizzes = filter_all_quizzes.exclude(user_id=user.id)
|
||||
|
||||
# Favoriten mit mind. 1 Frage
|
||||
favorite_quiz_ids = []
|
||||
favorite_quizzes = QivipQuiz.objects.none()
|
||||
if user:
|
||||
favorite_quiz_ids = QivipQuizFavorite.objects.filter(user=user, favorite=True).values_list('quiz', flat=True)
|
||||
favorite_quizzes = QivipQuiz.objects.annotate(max_amount_questions=Count('questions')).filter(
|
||||
id__in=favorite_quiz_ids,
|
||||
max_amount_questions__gte=1
|
||||
)
|
||||
filter_my_quizzes = QivipQuiz.objects.none()
|
||||
filter_other_quizzes=QivipQuiz.objects.none()
|
||||
|
||||
# Suche anwenden, falls vorhanden
|
||||
|
||||
|
||||
filter_my_quizzes = apply_quiz_filters(filter_my_quizzes, form)
|
||||
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form)
|
||||
favorite_quizzes = apply_quiz_filters(favorite_quizzes, form)
|
||||
if request.user.is_authenticated:
|
||||
filter_all_quizzes = apply_quiz_filters(filter_all_quizzes, form) | apply_quiz_filters(filter_my_quizzes, form)
|
||||
|
||||
# Treffer zählen
|
||||
total_my = filter_my_quizzes.count()
|
||||
total_all = filter_all_quizzes.count()
|
||||
total_favorite = favorite_quizzes.count()
|
||||
|
||||
total_all_found=total_my + total_all + total_favorite
|
||||
|
||||
# Paginierung
|
||||
paginator_my = Paginator(filter_my_quizzes.order_by('-creation_date'), 8)
|
||||
paginator_all = Paginator(filter_all_quizzes.order_by('-published_at'), 8)
|
||||
paginator_fav = Paginator(favorite_quizzes.order_by('-published_at'), 8)
|
||||
|
||||
my_page = request.GET.get('my_page', 1)
|
||||
all_page = request.GET.get('all_page', 1)
|
||||
fav_page = request.GET.get('favorite_page', 1)
|
||||
if search:
|
||||
filters['name__icontains'] = search
|
||||
if min_amout_questions:
|
||||
filters['max_amout_questions__gte'] = min_amout_questions
|
||||
if max_amout_questions:
|
||||
filters['max_amout_questions__lte'] = max_amout_questions
|
||||
|
||||
try:
|
||||
my_quizzes = paginator_my.page(my_page)
|
||||
filter_other_quizzes = filter_other_quizzes.filter(**filters)
|
||||
except:
|
||||
my_quizzes = paginator_my.page(1)
|
||||
filter_other_quizzes=QivipQuiz.objects.none()
|
||||
try:
|
||||
filter_my_quizzes = filter_my_quizzes.filter(**filters)
|
||||
except:
|
||||
filter_my_quizzes=QivipQuiz.objects.none()
|
||||
|
||||
try:
|
||||
all_quizzes = paginator_all.page(all_page)
|
||||
favorite_quizzes = favorite_quizzes.filter(**filters)
|
||||
except:
|
||||
all_quizzes = paginator_all.page(1)
|
||||
favorite_quizzes=QivipQuiz.objects.none()
|
||||
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
try:
|
||||
favorite_quizzes = paginator_fav.page(fav_page)
|
||||
favorite_quizzes = favorite_quizzes.filter(
|
||||
favorites__user=request.user, # Filtere nach dem User
|
||||
favorites__favorite=True # Filtere nach favorisierten Quizzes
|
||||
).order_by('-favorites__favorite_date')
|
||||
except:
|
||||
favorite_quizzes = paginator_fav.page(1)
|
||||
pass
|
||||
|
||||
|
||||
# Pagination for favorite_quizzes
|
||||
favorite_quizzes_paginator = Paginator(favorite_quizzes, 8) # 8 quizzes per page
|
||||
try:
|
||||
favorite_quizzes = favorite_quizzes_paginator.page(request.GET.get('my_page', 1))
|
||||
except:
|
||||
favorite_quizzes = favorite_quizzes_paginator.page(1)
|
||||
|
||||
context = {
|
||||
'critisized_quizzes':critisized_quizzes,
|
||||
'form': form,
|
||||
'my_quizzes': my_quizzes,
|
||||
'all_quizzes': all_quizzes,
|
||||
'favorite_quizzes': favorite_quizzes,
|
||||
'total_my': total_my,
|
||||
'total_all': total_all,
|
||||
'total_favorite': total_favorite,
|
||||
'total_all_found':total_all_found,
|
||||
'show_search': True,
|
||||
'favorite_quiz_ids': list(favorite_quiz_ids),
|
||||
'url_my': '/library/my/',
|
||||
'url_all': '/library/',
|
||||
'url_favorite': '/library/favorites/',
|
||||
'favorite_quiz_ids': favorite_quiz_ids,
|
||||
'favorite_quizzes':favorite_quizzes,
|
||||
'my_quizzes': my_quizzes,
|
||||
'other_quizzes': other_quizzes,
|
||||
'form': form,
|
||||
}
|
||||
|
||||
query_params = request.GET.copy()
|
||||
|
||||
querydict_my = query_params.copy()
|
||||
querydict_all = query_params.copy()
|
||||
querydict_fav = query_params.copy()
|
||||
|
||||
# Entferne die jeweiligen Seiten-Parameter, damit wir sie neu setzen können
|
||||
querydict_my.pop('my_page', None)
|
||||
querydict_all.pop('all_page', None)
|
||||
querydict_fav.pop('favorite_page', None)
|
||||
|
||||
# Füge die "bereinigten" Querystrings dem context hinzu
|
||||
context.update({
|
||||
'querystring_my': querydict_my.urlencode(),
|
||||
'querystring_all': querydict_all.urlencode(),
|
||||
'querystring_favorite': querydict_fav.urlencode(),
|
||||
})
|
||||
|
||||
if connect=="all":
|
||||
context.update({"link":"library:overview_quiz"})
|
||||
return render(request, 'library/overview_quiz.html', context)
|
||||
elif connect=="my":
|
||||
context.update({"link":"library:overview_quiz_my"})
|
||||
return render(request, 'library/overview_quiz_my.html', context)
|
||||
elif connect=="favorite":
|
||||
context.update({"link":"library:overview_quiz_favorites"})
|
||||
return render(request, 'library/overview_quiz_favorite.html', context)
|
||||
|
||||
|
||||
def overview_quiz_my(request):
|
||||
return overview_quiz(request, connect="my")
|
||||
|
||||
|
||||
def overview_quiz_favorites(request):
|
||||
return overview_quiz(request, connect="favorite")
|
||||
|
||||
# Neues Quiz erstellen
|
||||
@login_required
|
||||
def favorite_quiz(request, pk):
|
||||
quiz= get_object_or_404(QivipQuiz, pk=pk)
|
||||
@@ -353,7 +163,6 @@ def favorite_quiz(request, pk):
|
||||
|
||||
|
||||
|
||||
|
||||
if favorite.favorite==True:
|
||||
favorite.favorite_date = timezone.now()
|
||||
favorite.save()
|
||||
@@ -368,7 +177,6 @@ def favorite_quiz(request, pk):
|
||||
favorite=QivipQuizFavorite.objects.create(user=request.user, quiz=quiz, favorite=True, favorite_date=timezone.now())
|
||||
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich zu den Favoriten hinzugefügt!"
|
||||
messages.success(request,statement )
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER', 'library:overview_quiz'))
|
||||
|
||||
|
||||
@@ -381,19 +189,10 @@ def new_quiz(request):
|
||||
|
||||
|
||||
form = QuizForm(request.POST, request.FILES)
|
||||
|
||||
|
||||
|
||||
if form.is_valid():
|
||||
quiz = form.save(commit=False)
|
||||
status = form.cleaned_data.get('status')
|
||||
|
||||
if status == 'öffentlich' and not quiz.published_at:
|
||||
quiz.published_at = timezone.now()
|
||||
|
||||
try:
|
||||
quiz_image=request.FILES['quiz_image']
|
||||
|
||||
imagequiz=QuizImage.objects.create(image=quiz_image)
|
||||
quiz.image=imagequiz
|
||||
|
||||
@@ -402,8 +201,6 @@ def new_quiz(request):
|
||||
|
||||
|
||||
quiz.user_id = request.user
|
||||
|
||||
|
||||
#quiz.creator = request.user
|
||||
quiz.save()
|
||||
#form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
|
||||
@@ -419,13 +216,8 @@ def edit_quiz(request, pk):
|
||||
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
|
||||
if request.method == 'POST':
|
||||
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
|
||||
|
||||
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
|
||||
|
||||
if 'reset_image' in request.POST:
|
||||
quiz.image = None
|
||||
|
||||
@@ -438,12 +230,7 @@ def edit_quiz(request, pk):
|
||||
except:
|
||||
pass
|
||||
|
||||
quiz = form.save(commit=False)
|
||||
|
||||
# Beispiel: Status aus dem Formular holen
|
||||
status = form.cleaned_data.get('status')
|
||||
if status == 'öffentlich' and not quiz.published_at:
|
||||
quiz.published_at = timezone.now()
|
||||
|
||||
form.save()
|
||||
for img in QuizImage.objects.all():
|
||||
@@ -481,11 +268,8 @@ def delete_quiz(request, pk):
|
||||
return redirect('library:overview_quiz')
|
||||
return render(request, 'library/delete_confirmation.html', {'object': quiz})
|
||||
|
||||
|
||||
|
||||
# Quiz anzeigen
|
||||
def detail_quiz(request, pk):
|
||||
critisize_quiz(request, pk)
|
||||
quiz = get_object_or_404(QivipQuiz, pk=pk)
|
||||
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
|
||||
questions = QivipQuestion.objects.filter(quiz_id=quiz)
|
||||
@@ -524,14 +308,6 @@ def copy_quiz(request, pk):
|
||||
new_quiz.base_quiz_id = pk
|
||||
new_quiz.rating_count = 0
|
||||
new_quiz.average_rating = 3.0
|
||||
new_quiz.quiz_played_Lernmodus = 0
|
||||
new_quiz.quiz_played_Moderiermodus = 0
|
||||
new_quiz.quiz_played_Moderiermodus_players = 0
|
||||
if original_quiz.status=="öffentlich":
|
||||
new_quiz.published_at = timezone.now()
|
||||
else:
|
||||
new_quiz.published_at = None
|
||||
|
||||
new_quiz.save() # Speichern als neues Objekt
|
||||
# Optional: Beschreibung anpassen
|
||||
# Alle zugehörigen Fragen kopieren
|
||||
@@ -565,7 +341,6 @@ def question_list(request):
|
||||
def new_question(request):
|
||||
question_type = request.GET.get('type')
|
||||
quiz_id = request.GET.get('quiz_id')
|
||||
question_data = None
|
||||
|
||||
if not quiz_id:
|
||||
messages.error(request, 'Quiz ID muss angegeben werden.')
|
||||
@@ -577,12 +352,12 @@ def new_question(request):
|
||||
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
|
||||
return redirect('library:overview_quiz')
|
||||
|
||||
if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']:
|
||||
if question_type not in ['multiple_choice', 'true_false','input']:
|
||||
base_url = reverse('library:new_question')
|
||||
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
||||
|
||||
if request.method == 'POST':
|
||||
question_text = request.POST.get('question', 'Kein Fragentext angegeben')
|
||||
question_text = request.POST.get('question', '')
|
||||
|
||||
# Handle different question types
|
||||
if question_type == 'true_false':
|
||||
@@ -630,59 +405,6 @@ def new_question(request):
|
||||
'options': options
|
||||
}
|
||||
|
||||
elif question_type == 'order' :
|
||||
options = []
|
||||
i = 1
|
||||
# Limit to maximum 6 options
|
||||
while request.POST.get(f'option_{i}') and i <= 6:
|
||||
is_correct = request.POST.get(f'correct_{i}') == '1'
|
||||
options.append({
|
||||
'order': i,
|
||||
'value': request.POST.get(f'option_{i}'),
|
||||
|
||||
})
|
||||
i += 1
|
||||
|
||||
|
||||
# Validate minimum 2 options
|
||||
if len(options) < 2:
|
||||
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'options': options
|
||||
}
|
||||
|
||||
|
||||
elif question_type == 'guess':
|
||||
correct_answer = request.POST.get('correct_answer')
|
||||
min_value = request.POST.get('min')
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
|
||||
elif question_type == 'map':
|
||||
target_location = request.POST.get('location')
|
||||
tolerance_radius = request.POST.get('tolerance_radius')
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'target_location': target_location,
|
||||
'tolerance_radius': tolerance_radius
|
||||
}
|
||||
question = QivipQuestion()
|
||||
question.data = json.dumps(json_data)
|
||||
question.quiz_id = quiz
|
||||
@@ -695,10 +417,6 @@ def new_question(request):
|
||||
|
||||
|
||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||
try:
|
||||
question.credits=request.POST.get('credits',"")
|
||||
except:
|
||||
question.credits=""
|
||||
question.save()
|
||||
#return modified(request, pk=quiz.pk)
|
||||
return redirect('library:detail_quiz', pk=quiz.pk)
|
||||
@@ -737,43 +455,6 @@ def new_question(request):
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
|
||||
elif question_type == 'order':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'options': [
|
||||
{'value': ''},
|
||||
{'value': ''}
|
||||
]
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
elif question_type == 'guess':
|
||||
correct_answer = request.POST.get('correct_answer')
|
||||
min_value = request.POST.get('min')
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
|
||||
elif question_type == 'map':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'target_location': "",
|
||||
'tolerance_radius': 90000
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
|
||||
template_name = f'library/question/question_{question_type}.html'
|
||||
context = {
|
||||
'question': question_data,
|
||||
@@ -814,13 +495,6 @@ def edit_question(request, pk):
|
||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||
]
|
||||
}
|
||||
elif question_type == 'map':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': question_data.get('question', ''),
|
||||
'target_location': question_data.get('target_location', ''),
|
||||
'tolerance_radius': question_data.get('tolerance_radius', 90000)
|
||||
}
|
||||
else:
|
||||
# For multiple choice questions
|
||||
question_data.setdefault('type', question_type)
|
||||
@@ -906,65 +580,9 @@ def edit_question(request, pk):
|
||||
'options': options
|
||||
}
|
||||
|
||||
elif question_type == 'order':
|
||||
options = []
|
||||
i = 1
|
||||
# Limit to maximum 6 options
|
||||
while request.POST.get(f'option_{i}') and i <= 6:
|
||||
options.append({
|
||||
'order': i,
|
||||
'value': request.POST.get(f'option_{i}')
|
||||
})
|
||||
i += 1
|
||||
|
||||
# Validate minimum 2 options
|
||||
if len(options) < 2:
|
||||
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
# Validate maximum 6 options
|
||||
if len(options) > 6:
|
||||
messages.error(request, 'Maximal 6 Optionen sind erlaubt.')
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'options': options
|
||||
}
|
||||
elif question_type == 'guess':
|
||||
correct_answer = request.POST.get('correct_answer')
|
||||
min_value = request.POST.get('min')
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
|
||||
elif question_type == 'map':
|
||||
target_location = request.POST.get('location')
|
||||
tolerance_radius = request.POST.get('tolerance_radius')
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'target_location': target_location,
|
||||
'tolerance_radius': tolerance_radius
|
||||
}
|
||||
question.data = json.dumps(json_data)
|
||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||
try:
|
||||
question.credits=request.POST.get('credits',"")
|
||||
except:
|
||||
question.credits=""
|
||||
|
||||
|
||||
question.save()
|
||||
for img in QuestionImage.objects.all():
|
||||
@@ -979,7 +597,6 @@ def edit_question(request, pk):
|
||||
else:
|
||||
template_name = f'library/question/question_{question_type}.html'
|
||||
context = {
|
||||
'credits':question.credits,
|
||||
'question': {'data': question_data}, # Wrap in data structure expected by template
|
||||
'quiz_id': question.quiz_id.pk,
|
||||
'quiz': question.quiz_id,
|
||||
|
||||
573
django/package-lock.json
generated
573
django/package-lock.json
generated
@@ -10,11 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.1.5",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwindcss": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"license-checker": "^25.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
@@ -541,55 +537,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/array-find-index": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||
"integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
@@ -601,60 +548,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/debuglog": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
|
||||
"integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
@@ -666,16 +559,6 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
@@ -688,15 +571,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -708,106 +582,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -843,33 +622,6 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/license-checker": {
|
||||
"version": "25.0.1",
|
||||
"resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz",
|
||||
"integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"debug": "^3.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nopt": "^4.0.1",
|
||||
"read-installed": "~4.0.3",
|
||||
"semver": "^5.5.0",
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-satisfies": "^4.0.0",
|
||||
"treeify": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"license-checker": "bin/license-checker"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||
@@ -1107,39 +859,6 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -1148,101 +867,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/osenv": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1259,177 +888,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/read-installed": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz",
|
||||
"integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debuglog": "^1.0.1",
|
||||
"read-package-json": "^2.0.0",
|
||||
"readdir-scoped-modules": "^1.0.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"slide": "~1.1.3",
|
||||
"util-extend": "^1.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/read-package-json": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz",
|
||||
"integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==",
|
||||
"deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"normalize-package-data": "^2.0.0",
|
||||
"npm-normalize-package-bin": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-scoped-modules": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
|
||||
"integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==",
|
||||
"deprecated": "This functionality has been moved to @npmcli/fs",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debuglog": "^1.0.1",
|
||||
"dezalgo": "^1.0.0",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"once": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/slide": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
|
||||
"integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
|
||||
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A=="
|
||||
},
|
||||
"node_modules/spdx-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-find-index": "^1.0.2",
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-ranges": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-exceptions": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
||||
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-expression-parse": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-license-ids": {
|
||||
"version": "3.0.21",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
|
||||
"integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-ranges": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz",
|
||||
"integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-satisfies": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz",
|
||||
"integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-compare": "^1.0.0",
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-ranges": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz",
|
||||
@@ -1453,37 +911,6 @@
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/treeify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz",
|
||||
"integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/util-extend": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
|
||||
"integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.1.5",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwindcss": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"license-checker": "^25.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from play.models import QuizGame, QuizGameParticipant, QuizAnswer,QivipQuiz
|
||||
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
||||
from django.conf import settings
|
||||
from geopy.distance import geodesic
|
||||
|
||||
class GameConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
@@ -32,13 +31,6 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
# Check if game should be deleted
|
||||
await self.cleanup_game()
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_question_data(self):
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||
return json.loads(current_question.data)
|
||||
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message_type = text_data_json['type']
|
||||
@@ -92,17 +84,15 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
elif message_type == 'submit_answer':
|
||||
participant_id = text_data_json['participant_id']
|
||||
try:
|
||||
answer = text_data_json['answer']
|
||||
except:
|
||||
answer=-1
|
||||
time_remaining = text_data_json.get('time_remaining', 0)
|
||||
|
||||
|
||||
question_data = await self.get_question_data()
|
||||
if question_data['type'] == "map":
|
||||
await self.save_answer_map(participant_id, answer, time_remaining)
|
||||
else:
|
||||
# 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,
|
||||
@@ -121,7 +111,7 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
stats = await self.get_answer_stats()
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'answer_stats',
|
||||
'stats': stats,
|
||||
'stats': stats
|
||||
}))
|
||||
|
||||
elif message_type == 'update_participants':
|
||||
@@ -138,18 +128,8 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
host_id = text_data_json['host_id']
|
||||
if await self.verify_host(host_id):
|
||||
await self.advance_to_next_question()
|
||||
elif message_type == 'winner_podest':
|
||||
host_id = text_data_json['host_id']
|
||||
if await self.verify_host(host_id):
|
||||
await self.show_winner_podest()
|
||||
|
||||
elif message_type == 'scoreboard':
|
||||
host_id = text_data_json['host_id']
|
||||
if await self.verify_host(host_id):
|
||||
await self.show_scoreboard()
|
||||
|
||||
elif message_type == 'finish_game':
|
||||
|
||||
host_id = text_data_json['host_id']
|
||||
if await self.verify_host(host_id):
|
||||
await self.finish_game()
|
||||
@@ -194,139 +174,7 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
return False
|
||||
|
||||
@database_sync_to_async
|
||||
def save_answer_map(self, participant_id, answer_location, time_remaining):
|
||||
answer_index = 0
|
||||
try:
|
||||
# Get all necessary data first
|
||||
participant = QuizGameParticipant.objects.get(
|
||||
quiz_game__join_code=self.join_code,
|
||||
participant_id=participant_id
|
||||
)
|
||||
quiz_game = participant.quiz_game
|
||||
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||
question_data = json.loads(current_question.data)
|
||||
target_location = question_data['target_location']
|
||||
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
|
||||
|
||||
|
||||
# Parse coordinates if they are strings
|
||||
def parse_coords(coord):
|
||||
if isinstance(coord, str):
|
||||
try:
|
||||
# Handle both comma and space separated coordinates
|
||||
if ',' in coord:
|
||||
lat, lng = map(float, [x.strip() for x in coord.split(',')])
|
||||
else:
|
||||
# Try splitting by space if no comma
|
||||
parts = coord.strip().split()
|
||||
if len(parts) >= 2:
|
||||
lat, lng = map(float, parts[:2])
|
||||
else:
|
||||
raise ValueError("Invalid coordinate format")
|
||||
return (lat, lng)
|
||||
except (ValueError, AttributeError) as e:
|
||||
print(f"Error parsing coordinates {coord}: {e}")
|
||||
return None
|
||||
elif isinstance(coord, (list, tuple)) and len(coord) >= 2:
|
||||
return (float(coord[0]), float(coord[1]))
|
||||
return None
|
||||
|
||||
# Parse the coordinates
|
||||
target_location = parse_coords(target_location)
|
||||
answer_location_coords = answer_location
|
||||
answer_location = parse_coords(answer_location)
|
||||
|
||||
|
||||
if not target_location or not answer_location:
|
||||
print("Invalid coordinates, cannot calculate score")
|
||||
is_correct = False
|
||||
score = 0
|
||||
else:
|
||||
|
||||
# Configuration - Make these easily adjustable
|
||||
MAX_DISTANCE_KM = 50 # Maximum distance for scoring (in km)
|
||||
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
|
||||
|
||||
try:
|
||||
# Calculate distance
|
||||
distance_km = geodesic(answer_location, target_location).kilometers
|
||||
tolerance_km = float(tolerance_radius) / 1000 # Convert meters to kilometers
|
||||
|
||||
print(f"Distance: {distance_km} km, Tolerance: {tolerance_km} km")
|
||||
|
||||
# Calculate base score with non-linear decrease
|
||||
if distance_km <= tolerance_km:
|
||||
# Direct hit - full points + bonus
|
||||
base_score = 1000 + DIRECT_HIT_BONUS
|
||||
is_correct = True
|
||||
else:
|
||||
if distance_km > MAX_DISTANCE_KM:
|
||||
# Beyond max distance - 0 points
|
||||
base_score = 0
|
||||
is_correct = False
|
||||
else:
|
||||
# Non-linear decrease using exponential falloff
|
||||
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
|
||||
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
|
||||
base_score = 1000 * (1 - falloff)
|
||||
is_correct = True
|
||||
|
||||
# Apply time penalty (reduces score based on time taken)
|
||||
if is_correct and base_score > 0:
|
||||
# Calculate time taken as a fraction of total time (0-1)
|
||||
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
|
||||
max_time_sec = int(current_question.time_per_question)
|
||||
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
|
||||
|
||||
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
|
||||
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
|
||||
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
|
||||
else:
|
||||
final_score = base_score # 0 points for incorrect answers
|
||||
|
||||
# Apply max score limit after all calculations (including bonus)
|
||||
final_score = min(final_score, 1200) # Cap at 1200 (1000 + 200 bonus)
|
||||
|
||||
score = int(round(final_score))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calculating score: {e}")
|
||||
is_correct = False
|
||||
score = 0
|
||||
|
||||
# Save the answer
|
||||
answer_obj, created = QuizAnswer.objects.get_or_create(
|
||||
participant=participant,
|
||||
question_index=quiz_game.current_question_index,
|
||||
defaults={
|
||||
'answer_index': answer_index,
|
||||
'is_correct': is_correct,
|
||||
'score': score,
|
||||
'time_remaining': time_remaining
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
answer_obj.answer_index = answer_index
|
||||
answer_obj.is_correct = is_correct
|
||||
answer_obj.score = score
|
||||
answer_obj.time_remaining = time_remaining
|
||||
answer_obj.save()
|
||||
|
||||
participant.last_score = score
|
||||
participant.score += score
|
||||
participant.coordinate_answer= answer_location_coords
|
||||
participant.last_answer_correct = is_correct
|
||||
participant.save()
|
||||
|
||||
return True
|
||||
|
||||
except (QuizGameParticipant.DoesNotExist, IndexError):
|
||||
return False
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def save_answer(self, participant_id, answer, time_remaining):
|
||||
def save_answer(self, participant_id, answer_index, time_remaining):
|
||||
try:
|
||||
participant = QuizGameParticipant.objects.get(
|
||||
quiz_game__join_code=self.join_code,
|
||||
@@ -335,95 +183,58 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
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
|
||||
question_data = json.loads(current_question.data)
|
||||
question_type = question_data.get("type")
|
||||
|
||||
is_correct = False
|
||||
answer_index = None
|
||||
answers_order = None
|
||||
answer_text = None
|
||||
|
||||
if question_type in ["multiple_choice", "true_false"]:
|
||||
# answer ist hier eine Integer-Antwort (Index)
|
||||
answer_index = int(answer)
|
||||
if answer_index >= 0:
|
||||
is_correct = question_data['options'][answer_index].get('is_correct', False)
|
||||
|
||||
elif question_type == "input":
|
||||
user_input = str(answer).strip().lower().replace(" ", "")
|
||||
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
|
||||
is_correct = (user_input == correct_value)
|
||||
answer_text = user_input
|
||||
answer_index = 0 if is_correct else 1
|
||||
|
||||
elif question_type == "order":
|
||||
# answer ist hier eine Liste von Strings mit der Reihenfolge
|
||||
answers_order = answer
|
||||
correct_sequence = sorted(question_data["options"], key=lambda x: x["order"])
|
||||
correct_values = [opt["value"] for opt in correct_sequence]
|
||||
is_correct = (answers_order == correct_values)
|
||||
|
||||
elif question_type == "guess":
|
||||
print(answer_index)
|
||||
try:
|
||||
user_val = float(answer)
|
||||
correct_val = float(question_data['options'][0].get('value', 0))
|
||||
tolerance = float(question_data.get('tolerance', 0))
|
||||
percentage = 1 - abs(user_val - correct_val) / (abs(correct_val) + 1e-5)
|
||||
|
||||
|
||||
score_percentage= min(round(percentage, 3), 1.0)
|
||||
if score_percentage>= abs(tolerance/100):
|
||||
is_correct=True
|
||||
else:
|
||||
is_correct=False
|
||||
|
||||
print(score_percentage)
|
||||
|
||||
|
||||
except ValueError:
|
||||
is_correct = False # falls keine Zahl eingegeben wurde
|
||||
answer_index = 0 if is_correct else 1
|
||||
|
||||
|
||||
# Score berechnen
|
||||
try:
|
||||
if score_percentage==None:
|
||||
score_percentage=1
|
||||
if answer_index >= 0 : # -1 means timeout
|
||||
is_correct = question_data['options'][answer_index]['is_correct']
|
||||
print(is_correct)
|
||||
except:
|
||||
score_percentage=1
|
||||
user_input = str(answer_index).strip().lower().replace(" ", "")
|
||||
correct_value = str(question_data['options'][0].get('value', '')).strip().lower().replace(" ", "")
|
||||
print("Richtig",correct_value)
|
||||
print(user_input)
|
||||
if user_input == correct_value:
|
||||
is_correct=True
|
||||
answer_index=int(0)
|
||||
print(is_correct)
|
||||
else:
|
||||
answer_index=int(1)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Calculate score based on correctness and time
|
||||
score = 0
|
||||
if is_correct:
|
||||
base_score = 1000
|
||||
time_factor = time_remaining / int(current_question.time_per_question) / 1000
|
||||
score = int(base_score * (0.75 + 0.25 * time_factor))*score_percentage
|
||||
print(score)
|
||||
time_factor = time_remaining / int(current_question.time_per_question)/int(1000)
|
||||
score = int(base_score * (0.5 + 0.5 * time_factor))
|
||||
|
||||
# Antwort speichern oder aktualisieren
|
||||
# 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,
|
||||
'answers_order': answers_order,
|
||||
'is_correct': is_correct,
|
||||
'score': score,
|
||||
'time_remaining': time_remaining
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
answer_obj.answer_index = answer_index
|
||||
answer_obj.answers_order = answers_order
|
||||
answer_obj.is_correct = is_correct
|
||||
answer_obj.score = score
|
||||
answer_obj.time_remaining = time_remaining
|
||||
answer_obj.save()
|
||||
|
||||
# Teilnehmer aktualisieren
|
||||
participant.last_score = score
|
||||
participant.score += score
|
||||
participant.last_answer=answer_index
|
||||
participant.last_answer_correct = is_correct
|
||||
participant.save()
|
||||
|
||||
@@ -431,50 +242,28 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
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
|
||||
|
||||
|
||||
|
||||
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||
|
||||
question_data = json.loads(current_question.data)
|
||||
question_type = question_data.get("type")
|
||||
stats = {}
|
||||
|
||||
for single_answer in answers:
|
||||
|
||||
|
||||
if question_type == "order":
|
||||
correct_answer=[str(opt["value"]) for opt in sorted(question_data["options"], key=lambda x: x["order"])]
|
||||
|
||||
if single_answer.answers_order== correct_answer:
|
||||
key = 0 # richtig
|
||||
else:
|
||||
key = 1 # falsch
|
||||
stats[key] = stats.get(key, 0) + 1
|
||||
else:
|
||||
key = str(single_answer.answer_index if single_answer.answer_index is not None else "unanswered")
|
||||
stats[key] = stats.get(key, 0) + 1
|
||||
return stats
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
except QuizGame.DoesNotExist:
|
||||
return {}
|
||||
|
||||
@database_sync_to_async
|
||||
def save_rating(self, participant_id, rating):
|
||||
from django.db import models
|
||||
if not settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']:
|
||||
return False
|
||||
from library.models import QuizRating
|
||||
@@ -486,23 +275,16 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
)
|
||||
|
||||
# Update or create rating
|
||||
"""
|
||||
rating_obj, created = QuizRating.objects.get_or_create(
|
||||
quiz=game.quiz_id,
|
||||
participant_id=participant_id,
|
||||
defaults={'rating': rating}
|
||||
)
|
||||
"""
|
||||
rating_obj=QuizRating.objects.create(
|
||||
quiz=game.quiz_id,
|
||||
participant_id=participant_id,
|
||||
rating=rating)
|
||||
|
||||
if not created:
|
||||
rating_obj.rating = rating
|
||||
rating_obj.save()
|
||||
|
||||
|
||||
|
||||
return True
|
||||
except (QuizGame.DoesNotExist, QuizGameParticipant.DoesNotExist):
|
||||
return False
|
||||
@@ -583,26 +365,6 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
elif result == 'winner_podest':
|
||||
# Notify clients to redirect to next question
|
||||
await self.channel_layer.group_send(
|
||||
self.game_group_name,
|
||||
{
|
||||
'type': 'game_state_update',
|
||||
'action': 'show_winner_podest',
|
||||
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
elif result == 'scoreboard':
|
||||
# Notify clients to redirect to next question
|
||||
await self.channel_layer.group_send(
|
||||
self.game_group_name,
|
||||
{
|
||||
'type': 'game_state_update',
|
||||
'action': 'show_scoreboard',
|
||||
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def _show_scores(self):
|
||||
@@ -627,34 +389,10 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
}
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def counter_played_quizzes(self): # Zähler, wie oft ein Quiz gespielt worden ist.
|
||||
|
||||
quiz = QuizGame.objects.get(join_code=self.join_code).quiz_id
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if quiz_game.single_player_or_multiple=="multiple":
|
||||
quiz.quiz_played_Moderiermodus+=1
|
||||
|
||||
active_participants = QuizGameParticipant.objects.filter(
|
||||
quiz_game=quiz_game
|
||||
).count()
|
||||
quiz.quiz_played_Moderiermodus_players+=active_participants
|
||||
|
||||
else:
|
||||
quiz.quiz_played_Lernmodus+=1
|
||||
quiz.save()
|
||||
|
||||
@database_sync_to_async
|
||||
def _finish_game(self):
|
||||
try:
|
||||
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
|
||||
quiz_game.current_state = 'finished'
|
||||
quiz_game.save()
|
||||
return True
|
||||
@@ -663,7 +401,6 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
async def finish_game(self):
|
||||
success = await self._finish_game()
|
||||
await self.counter_played_quizzes()
|
||||
if success:
|
||||
# Notify clients to redirect to finished page
|
||||
await self.channel_layer.group_send(
|
||||
@@ -674,48 +411,3 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
@database_sync_to_async
|
||||
def _show_winner_podest(self):
|
||||
try:
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
quiz_game.current_state = 'winner_podest'
|
||||
quiz_game.save()
|
||||
return True
|
||||
except QuizGame.DoesNotExist:
|
||||
return False
|
||||
|
||||
async def show_winner_podest(self):
|
||||
success = await self._show_winner_podest()
|
||||
if success:
|
||||
# Notify clients to redirect to finished page
|
||||
await self.channel_layer.group_send(
|
||||
self.game_group_name,
|
||||
{
|
||||
'type': 'game_state_update',
|
||||
'action': 'show_winner_podest',
|
||||
'redirect_url': reverse('play:winner_podest', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def _show_scoreboard(self):
|
||||
try:
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
quiz_game.current_state = 'scoreboard'
|
||||
quiz_game.save()
|
||||
return True
|
||||
except QuizGame.DoesNotExist:
|
||||
return False
|
||||
|
||||
async def show_scoreboard(self):
|
||||
success = await self._show_scoreboard()
|
||||
if success:
|
||||
# Notify clients to redirect to finished page
|
||||
await self.channel_layer.group_send(
|
||||
self.game_group_name,
|
||||
{
|
||||
'type': 'game_state_update',
|
||||
'action': 'show_scoreboard',
|
||||
'redirect_url': reverse('play:scoreboard', kwargs={'join_code': self.join_code})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ game_connections = {}
|
||||
|
||||
class LobbyConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
|
||||
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
async def connect(self):
|
||||
self.join_code = self.scope['url_route']['kwargs']['join_code']
|
||||
@@ -147,7 +147,7 @@ class LobbyConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
# Zustand laden
|
||||
try:
|
||||
r = redis.Redis(host='localhost', port=6380, db=0, decode_responses=True)
|
||||
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
|
||||
|
||||
# Alte Teilnehmer aus Redis holen
|
||||
data = r.get(f"last_participants_{self.room_group_name}")
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-18 13:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0012_quizgameparticipant_last_score'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizanswer',
|
||||
name='answer_text',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quizanswer',
|
||||
name='answers_order',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quizanswer',
|
||||
name='answer_index',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-25 16:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0013_quizanswer_answer_text_quizanswer_answers_order_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='quizgame',
|
||||
name='current_state',
|
||||
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet')], default='lobby', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-28 13:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0014_alter_quizgame_current_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='quizgame',
|
||||
name='current_state',
|
||||
field=models.CharField(choices=[('lobby', 'In Lobby'), ('question', 'Frage läuft'), ('scores', 'Punkteübersicht'), ('winner_podest', 'Siegerpodest'), ('finished', 'Beendet'), ('scoreboard', 'Scoreboard')], default='lobby', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-01 15:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0015_alter_quizgame_current_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizgameparticipant',
|
||||
name='coordinate_answer',
|
||||
field=models.CharField(blank=True, max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0016_quizgameparticipant_coordinate_answer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Lernmodus',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Moderiermodus',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Moderiermodus_players',
|
||||
field=models.IntegerField(blank=True, default=0, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 14:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0017_quizgame_quiz_played_lernmodus_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizgame',
|
||||
name='single_player_or_multiple',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 14:50
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0018_quizgame_single_player_or_multiple'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Lernmodus',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Moderiermodus',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quizgame',
|
||||
name='quiz_played_Moderiermodus_players',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quizgame',
|
||||
name='single_player_or_multiple',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-14 15:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0019_remove_quizgame_quiz_played_lernmodus_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizgame',
|
||||
name='single_player_or_multiple',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -10,12 +10,9 @@ class QuizGame(models.Model):
|
||||
('lobby', 'In Lobby'),
|
||||
('question', 'Frage läuft'),
|
||||
('scores', 'Punkteübersicht'),
|
||||
('winner_podest','Siegerpodest'),
|
||||
('finished', 'Beendet'),
|
||||
('scoreboard', 'Scoreboard'),
|
||||
|
||||
('finished', 'Beendet')
|
||||
]
|
||||
single_player_or_multiple=models.CharField(max_length=20, blank=True)
|
||||
|
||||
host_id = models.CharField(max_length=200, unique=True)
|
||||
join_code = models.CharField(max_length=6, unique=True, blank=True)
|
||||
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE)
|
||||
@@ -50,7 +47,6 @@ class QuizGameParticipant(models.Model):
|
||||
avatar = models.CharField(max_length=200, blank=True, default="")
|
||||
last_heartbeat = models.DateTimeField(auto_now_add=True)
|
||||
last_answer = models.IntegerField(null=True, blank=True)
|
||||
coordinate_answer = models.CharField(null=True, blank=True,max_length=256)
|
||||
last_answer_correct = models.BooleanField(null=True, blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -67,9 +63,7 @@ class QuizGameParticipant(models.Model):
|
||||
class QuizAnswer(models.Model):
|
||||
participant = models.ForeignKey(QuizGameParticipant, on_delete=models.CASCADE, related_name='answers')
|
||||
question_index = models.IntegerField()
|
||||
answer_index = models.IntegerField(null=True, blank=True) # für MC, TF
|
||||
answer_text = models.TextField(null=True, blank=True) # für Input
|
||||
answers_order = models.JSONField(null=True, blank=True) # NEU für "order"
|
||||
answer_index = models.IntegerField()
|
||||
is_correct = models.BooleanField()
|
||||
score = models.IntegerField()
|
||||
time_remaining = models.IntegerField()
|
||||
|
||||
@@ -12,8 +12,6 @@ urlpatterns = [
|
||||
path('game/<str:join_code>/waiting', views.waiting_room, name='waiting'),
|
||||
path('game/<str:join_code>/scores', views.scores, name='scores'),
|
||||
path('game/<str:join_code>/finished', views.finished, name='finished'),
|
||||
path('game/<str:join_code>/winner_podest', views.winner_podest, name='winner_podest'),
|
||||
path('game/<str:join_code>/scoreboard', views.scoreboard, name='scoreboard'),
|
||||
path('select_mode/<str:join_code>', views.select_mode, name='select_mode'),
|
||||
path('selected_mode/<str:join_code>', views.selected_mode, name='selected_mode'),
|
||||
]
|
||||
@@ -147,7 +147,26 @@ def join_game(request):
|
||||
messages.error(request, "Dieses Spiel existiert nicht.")
|
||||
return render(request, 'play/join_game.html')
|
||||
|
||||
def finished(request, join_code):
|
||||
try:
|
||||
game = QuizGame.objects.get(join_code=join_code)
|
||||
participant = None
|
||||
participant_id = request.session.get('participant_id')
|
||||
if participant_id:
|
||||
participant = QuizGameParticipant.objects.filter(
|
||||
quiz_game=game,
|
||||
participant_id=participant_id
|
||||
).first()
|
||||
|
||||
context = {
|
||||
'join_code': join_code,
|
||||
'is_host': str(game.host_id) == str(request.session.get('host_id')),
|
||||
'participant_id': participant_id if participant else None,
|
||||
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
|
||||
}
|
||||
return render(request, 'play/game/finished.html', context)
|
||||
except QuizGame.DoesNotExist:
|
||||
return redirect('home')
|
||||
|
||||
def scores(request, join_code):
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
@@ -189,51 +208,7 @@ def scores(request, join_code):
|
||||
'total_questions': len(questions)
|
||||
})
|
||||
|
||||
|
||||
def scoreboard(request, join_code):
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
|
||||
# Verify game state
|
||||
if quiz_game.current_state != 'scoreboard':
|
||||
return redirect('play:lobby', join_code=join_code)
|
||||
|
||||
# Get participants sorted by score
|
||||
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
|
||||
|
||||
# Check if user is host
|
||||
is_host = False
|
||||
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
|
||||
is_host = True
|
||||
|
||||
# Get current question for context
|
||||
questions = quiz_game.quiz_id.questions.all()
|
||||
current_question = questions[quiz_game.current_question_index]
|
||||
question_data = json.loads(current_question.data)
|
||||
|
||||
try:
|
||||
my_participant = request.session.get('my_participant-participant_id')
|
||||
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
|
||||
my_participant=participant
|
||||
except:
|
||||
participant = QuizGameParticipant.objects.none()
|
||||
|
||||
return render(request, 'play/game/scoreboard.html', {
|
||||
'quiz_game': quiz_game,
|
||||
'participants': participants,
|
||||
'participant': participant,
|
||||
'my_participant':my_participant,
|
||||
'is_host': is_host,
|
||||
'host_id': quiz_game.host_id if is_host else None,
|
||||
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
|
||||
'question_data': question_data,
|
||||
'question_index': quiz_game.current_question_index,
|
||||
'total_questions': len(questions)
|
||||
})
|
||||
|
||||
|
||||
"""
|
||||
def finished(request, join_code):
|
||||
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
|
||||
# Verify game state
|
||||
@@ -265,84 +240,6 @@ def finished(request, join_code):
|
||||
'is_host': is_host,
|
||||
'participant_id': participant_id if participant else None
|
||||
})
|
||||
"""
|
||||
|
||||
|
||||
def winner_podest(request, join_code):
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
|
||||
# Verify game state
|
||||
if quiz_game.current_state != 'winner_podest':
|
||||
return redirect('play:lobby', join_code=join_code)
|
||||
|
||||
# Get participants sorted by score
|
||||
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
|
||||
|
||||
# Check if user is host
|
||||
is_host = False
|
||||
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
|
||||
is_host = True
|
||||
|
||||
# Get current question for context
|
||||
questions = quiz_game.quiz_id.questions.all()
|
||||
current_question = questions[quiz_game.current_question_index]
|
||||
question_data = json.loads(current_question.data)
|
||||
|
||||
try:
|
||||
my_participant = request.session.get('my_participant-participant_id')
|
||||
participant = QuizGameParticipant.objects.get(participant_id=my_participant)
|
||||
my_participant=participant
|
||||
except:
|
||||
participant = QuizGameParticipant.objects.none()
|
||||
|
||||
return render(request, 'play/game/winner_podest.html', {
|
||||
'quiz_game': quiz_game,
|
||||
'participants': participants,
|
||||
'participant': participant,
|
||||
'my_participant':my_participant,
|
||||
'is_host': is_host,
|
||||
'host_id': quiz_game.host_id if is_host else None,
|
||||
'is_last_question': quiz_game.current_question_index + 1 >= len(questions),
|
||||
'question_data': question_data,
|
||||
'question_index': quiz_game.current_question_index,
|
||||
'total_questions': len(questions)
|
||||
})
|
||||
|
||||
def finished(request, join_code):
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
|
||||
if quiz_game.current_state != 'finished':
|
||||
return redirect('play:lobby', join_code=join_code)
|
||||
try:
|
||||
game = QuizGame.objects.get(join_code=join_code)
|
||||
# Get participants sorted by score
|
||||
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game).order_by('-score')
|
||||
|
||||
|
||||
# Get participant if exists
|
||||
participant_id = request.COOKIES.get('participant_id')
|
||||
participant = None
|
||||
if participant_id:
|
||||
participant = QuizGameParticipant.objects.filter(
|
||||
quiz_game=quiz_game,
|
||||
participant_id=participant_id
|
||||
).first()
|
||||
|
||||
is_host = False
|
||||
if 'host_id' in request.COOKIES and request.COOKIES['host_id'] == quiz_game.host_id:
|
||||
is_host = True
|
||||
|
||||
context = {
|
||||
'join_code': join_code,
|
||||
'is_host': is_host,
|
||||
'participants': participants,
|
||||
'participant_id': participant_id if participant else None,
|
||||
'rating_enabled': settings.QIVIP_CONFIG['ENABLE_RATING_SYSTEM']
|
||||
}
|
||||
return render(request, 'play/game/finished.html', context)
|
||||
except QuizGame.DoesNotExist:
|
||||
return redirect('home')
|
||||
|
||||
|
||||
def question(request, join_code):
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
@@ -420,38 +317,13 @@ def waiting_room(request, join_code):
|
||||
|
||||
def selected_mode(request, join_code):
|
||||
|
||||
|
||||
mode = request.GET.get('mode','default')
|
||||
request.session['mode'] = mode
|
||||
|
||||
quiz_game = QuizGame.objects.get(join_code=join_code)
|
||||
if mode=="learn":
|
||||
|
||||
quiz_game.single_player_or_multiple="single"
|
||||
else:
|
||||
|
||||
quiz_game.single_player_or_multiple="multiple"
|
||||
|
||||
quiz_game.save()
|
||||
|
||||
|
||||
quiz_game = get_object_or_404(QuizGame, join_code=join_code)
|
||||
quiz = get_object_or_404(QivipQuiz, pk=quiz_game.quiz_id.id)
|
||||
QuizGameParticipant.objects.filter(quiz_game=quiz_game).delete()
|
||||
|
||||
# Hier werden die Fragen gemischt!
|
||||
import random
|
||||
questions = quiz_game.quiz_id.questions.all()
|
||||
for question in questions:
|
||||
try:
|
||||
data = json.loads(question.data)
|
||||
if (data.get('type') == 'order' or data.get('type') == 'multiple_choice') and 'options' in data:
|
||||
|
||||
random.shuffle(data['options'])
|
||||
question.data = json.dumps(data)
|
||||
question.save(update_fields=['data'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = {
|
||||
'quiz': quiz,
|
||||
'quiz_game': quiz_game,
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
.select2-container {
|
||||
@apply font-black!;
|
||||
}
|
||||
|
||||
|
||||
|
||||
qp-container {
|
||||
@apply max-w-xl w-full mx-auto;
|
||||
}
|
||||
.dark-form label {
|
||||
@apply dark:text-white!;
|
||||
}
|
||||
|
||||
nav div ul.qp-nav-list li {
|
||||
@apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200;
|
||||
@@ -62,34 +53,6 @@ a.qp-a-button-small {
|
||||
}
|
||||
|
||||
|
||||
.field-group form p {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.field-group form p label {
|
||||
@apply text-gray-900 font-semibold text-sm;
|
||||
}
|
||||
|
||||
.field-group form p input {
|
||||
@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.field-group form p select {
|
||||
@apply p-3 rounded-md bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.field-group form p textarea {
|
||||
@apply p-3 rounded-xl bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 min-h-[100px];
|
||||
}
|
||||
|
||||
.field-group form p button {
|
||||
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105
|
||||
transition duration-200 font-black shadow-md hover:shadow-lg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ul.messages li {
|
||||
@@ -159,12 +122,3 @@ div.qp-question-container {
|
||||
div.qp-question-container img {
|
||||
@apply max-w-[50vw] max-h-[40vh] mx-auto;
|
||||
}
|
||||
|
||||
div#mapquestion-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div#map {
|
||||
@apply md:w-[600px] lg:w-[900px] h-[400px]
|
||||
}
|
||||
2
django/static/js/sortablejs/Sortable.min.js
vendored
2
django/static/js/sortablejs/Sortable.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,35 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto mt-20 p-6 bg-white shadow-xl rounded-2xl text-center dark:bg-transparent">
|
||||
|
||||
<div class="flex justify-center items-center gap-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-10 h-10 text-red-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||
</svg>
|
||||
<h1 class=" text-4xl font-extrabold text-red-600">404 Fehler</h1>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="my-6 flex justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-24 h-24 text-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-semibold text-gray-800 mb-2 dark:text-white ">Seite nicht gefunden</h2>
|
||||
<p class="text-lg text-gray-600 mb-6 dark:text-white">Ups, die angeforderte Seite wurde nicht gefunden. Überprüfen Sie die URL oder kehren Sie zur Startseite zurück.</p>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<a href="{% url 'homepage:home' %}"
|
||||
class="bg-blue-600 hover:bg-blue-700 transition-colors text-white px-6 py-3 rounded-full text-lg font-semibold shadow-lg">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="flex justify-center items-center mt-12 mx-auto px-10">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent! dark:text-white! ">
|
||||
<div class="grid place-content-center h-full mb-4 flex-col items-center justify-between">
|
||||
<div class="flex justify-center items-center mt-12 ">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="grid place-content-center h-120">
|
||||
|
||||
<h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1>
|
||||
|
||||
@@ -16,46 +16,10 @@
|
||||
</form>
|
||||
<button class="text-center text-sm w-full mt-4 font-light text-blue-600"><a href="{% url 'accounts:password_change' %}">Passwort ändern</a></button>
|
||||
<button class="text-center text-sm w-full mt-4 font-light text-red-600"><a href="{% url 'accounts:delete_account' %}">Account löschen</a></button>
|
||||
|
||||
<!-- Kritik eigener Quizze -->
|
||||
{% if own_critisized_quizzes %}
|
||||
<div class="border-2 border-red-600 p-6 rounded-xl shadow-inner bg-red-50 flex flex-col gap-4 items-start mt-8 dark:bg-transparent">
|
||||
<div class="flex items-center gap-2 text-xl font-semibold text-red-800 dark:text-red-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
Kritik
|
||||
</div>
|
||||
<p class="text-xs opacity-50 dark:text-white">
|
||||
Diese Quizze wurden von anderen Nutzern kritisiert, da sie als fehlerhaft oder nicht angemessen
|
||||
wahrgenommen wurden. Bitte überprüfen und verbessern Sie Ihre Quizze so schnell wie möglich.</p>
|
||||
{% for own_critisized_quiz in own_critisized_quizzes %}
|
||||
<a href="{% url 'library:detail_quiz' own_critisized_quiz.id %}"> <span class="font-bold text-blue-600 hover:underline">{{ own_critisized_quiz }}</span> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="border-2 border-blue-600 p-6 rounded-xl shadow-inner bg-blue-50 flex flex-col gap-4 items-start mt-8 dark:bg-transparent">
|
||||
|
||||
<div class="flex items-center gap-2 text-xl font-semibold text-blue-800 dark: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="w-6 h-6 text-yellow-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" />
|
||||
</svg>
|
||||
Erfolge
|
||||
</div>
|
||||
|
||||
<ul class="list-disc pl-6 text-base text-gray-700 dark:text-white">
|
||||
<li><b>{{ my_quizzes_number }}</b> Quizze erstellt</li>
|
||||
<li><b>{{ my_questions_number }}</b> Fragen insgesamt erstellt</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent">qivip
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black dark:text-white">Anmeldung</h2>
|
||||
<h2 class="text-2xl text-center p-4 font-black">Anmeldung</h2>
|
||||
<form class="space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12">
|
||||
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black dark:text-white">Registrieren</h2>
|
||||
<h2 class="text-2xl text-center p-4 font-black">Registrieren</h2>
|
||||
|
||||
<form class=" space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" class="dark:bg-gray-900 h-full">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}">
|
||||
<meta charset="UTF-8">
|
||||
@@ -9,16 +9,11 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<main class="grow">
|
||||
<body>
|
||||
{% include 'partials/_nav.html' %}
|
||||
{% block content%}{% endblock %}
|
||||
{% block faqs%}{% endblock %}
|
||||
</main>
|
||||
{% include 'partials/_footer.html' %}
|
||||
|
||||
<!-- Toast Container -->
|
||||
@@ -73,12 +68,12 @@
|
||||
}, duration);
|
||||
}
|
||||
};
|
||||
|
||||
if (window.location.pathname !== "/library/") {
|
||||
localStorage.setItem("meine_seite_scroll", 0);}
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
<style>html {
|
||||
overflow-y: scroll;
|
||||
}</style>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,9 +4,9 @@
|
||||
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
|
||||
<div class="text-center">
|
||||
<h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2>
|
||||
<h3 class="mt-4 italic font-light sm:text-4xl text-md bg-gradient-to-r from-pink-500 via-purple-600 to-blue-700 bg-clip-text text-transparent">weil Lernen auch Spaß <br> machen kann!</h3>
|
||||
<h3 class="mt-4 italic font-extralight sm:text-4xl text-md">forever and forall</h3>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md dark:bg-gray-900 dark:border-0">
|
||||
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md">
|
||||
<a class="qp-a-button bg-green-500 text-white" href="{% url 'play:join_game' %}">Teilnehmen</a>
|
||||
<a class="qp-a-button bg-indigo-500 text-white" href="{% url 'library:overview_quiz' %}">Moderieren</a>
|
||||
<a class="qp-a-button bg-purple-500 text-white" href="{% url 'library:new_quiz' %}">Erstellen</a>
|
||||
@@ -22,45 +22,16 @@
|
||||
|
||||
</style>
|
||||
|
||||
{% if descriptions %}
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Was kann qivip?</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mb-16">
|
||||
|
||||
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
|
||||
Mit qivip kann man:
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 text-gray-700 dark:text-white ">
|
||||
{% for description in descriptions %}
|
||||
<p>
|
||||
<li class="ml-4"><span class="font-bold">{{ description.title }}</span> {{ description.description }}</li>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if faqs %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">FAQs</h2>
|
||||
<h2 class="text-2xl font-bold text-gray-900">FAQs</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-group !max-w-4xl tracking-wide dark:!bg-gray-900 dark:!text-white">
|
||||
<div class="input-group !max-w-4xl tracking-wide">
|
||||
<h1 class="font-black text-blue-600 flex justify-between gap-2 lg:text-lg">
|
||||
|
||||
|
||||
@@ -71,17 +42,126 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% for faq in faqs %}
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:bg-gray-50 dark:!bg-gray-900 dark:!text-white ">
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">{{ faq.question }}
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Was ist qivip?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">{{ faq.answer }}</p>
|
||||
<p class="text-blue-600">qivip ist eine Webseite zum Lernen! Es können vor allem Quizze erstellt, bearbeitet, kopiert und auch gespielt werden. </p>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Was sind die Vorteile von qivip?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">
|
||||
Einige Vorteile von qivip:
|
||||
<br>
|
||||
- Sie können Quizze moderieren ohne Account (siehe wie man Quizze mit mehreren Personen gemeinsam spielt)
|
||||
<br>
|
||||
- alle öffentlichen Quizze können auch ohne Account gespielt werden
|
||||
<br>
|
||||
- qivip ist zu 100% kostenlos
|
||||
<br>
|
||||
- es gibt keine Werbung
|
||||
<br>
|
||||
- qivip ist Open Source
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Ist qivip kostenlos?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Ja! qivip ist zu 100% kostenlos und es gibt keine versteckten Kosten.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Muss ich mich bei qivip registrieren, wenn ich ein Quiz spielen möchte?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Nein! qivip ist so ausgelegt, dass man auch ohne Account alle öffentlichen Quizze spielen kann.</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Kann ich ein Quiz von qivip auch alleine spielen?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Ja! Gehen Sie zur Bibliothek und klicken Sie auf den Button "spielen" (eines von Ihnen ausgewählten Quizzes). Wählen Sie anschließend bei den Spielmodi einfach den Lernmodus aus.</p>
|
||||
</details>
|
||||
</div>
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Wie kann ich ein Quiz erstellen?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Wichtig: Für diese Funktion müssen Sie angemeldet sein. Gehen Sie dafür auf die Startseite und klicken Sie auf "Erstellen". Nun wird Ihnen ein Formular angezeigt, dass Sie dann ausfüllen.
|
||||
Anschließend können Sie Fragen zu dem Quiz hinzufügen, indem Sie auf den Button eines Fragetyps klicken. </p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Wie kann ich mit mehreren Personen ein Quiz gemeinsam spielen?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">
|
||||
Wenn man ein Quiz mit mehreren Personen spielt, gibt es sowohl einen Moderator als auch die Teilnehmer.
|
||||
<br>
|
||||
<br>
|
||||
<b>Moderator: </b>Der Moderator selbst spielt nicht mit. Gehen Sie entweder auf die Startseite und klicken Sie auf "Moderieren" oder klicken einfach auf "spielen" (eines von Ihnen ausgewählten Quizzes) und dann auf Moderiermodus.
|
||||
Der Moderator kann das Spiel, wenn die Teilnehmer da sind, starten.
|
||||
<br>
|
||||
<br>
|
||||
<b>Teilnehmer: </b>Gehen Sie auf die Startseite und klicken Sie auf "Teilnehmen" und geben Sie dann den beim Moderator angegebenen Zahlencode ein. Anschließlich können Sie noch einen Anzeigenamen eingeben.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Wo steht unser Server?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Unser Server befindet sich in Deutschland, genauer gesagt in Nürnberg. </p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-2.5 !max-w-4xl hover:!bg-gray-50">
|
||||
<details>
|
||||
<summary class="flex justify-between cursor-pointer font-bold">Wer steckt hinter qivip?
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="text-blue-600">Wir sind eine kleine Gruppe von Schülern, die sich im Rahmen eines Enrichmenten Projektes zusammengefunden hat. </p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block content%}
|
||||
|
||||
<div class="flex justify-center items-center mt-12 ">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white ">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="grid place-content-center h-120">
|
||||
|
||||
<h1 class="text-center m-4 text-3xl lg:text-6xl">Impressum</b></h1>
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:bg-transparent ">qivip
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block content%}
|
||||
<div class="flex justify-center items-center mt-12 ">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md dark:!bg-transparent dark:!text-white">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="grid place-content-center h-120">
|
||||
|
||||
<h1 class="text-center m-4 text-3xl lg:text-6xl">Datenschutz-</br>erklärung</h1>
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white dark:!bg-transparent ">qivip
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-lg mx-auto mt-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-white">
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
{% if object.quiz_id %}
|
||||
Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
{% else %}
|
||||
|
||||
@@ -1,88 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
|
||||
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex flex-wrap space-y-2 items-center justify-between mb-8 dark:text-white">
|
||||
<div class="container mx-auto">
|
||||
<div class="container mx-auto px-4">
|
||||
|
||||
<h1 class="text-2xl mt-4 font-bold dark:text-white trucate">
|
||||
{{ quiz.name }}
|
||||
</h1>
|
||||
<div class="flex flex-wrap space-y-2 items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold mr-4 ">{{ quiz.name }}</h1>
|
||||
|
||||
<div class=" flex flex-wrap space-x-2 ">
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex items-center justify-center lg:justify-start gap-4 mt-4 flex-wrap">
|
||||
|
||||
<a href="{% url 'library:copy_quiz' quiz.id %}"
|
||||
class="inline-flex bg-green-100 text-green-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
|
||||
duplizieren
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
|
||||
{% if quiz.questions.count %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="inline-flex bg-blue-100 text-blue-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
|
||||
spielen
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="flex flex-wrap">
|
||||
{% if quiz.user_id == request.user %}
|
||||
<!-- Edit -->
|
||||
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}"
|
||||
class="inline-flex bg-orange-100 text-orange-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
|
||||
bearbeiten
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}"
|
||||
class="inline-flex bg-red-100 text-red-800 items-center py-2 px-2 gap-2 rounded-md text-sm lg:text-md hover:border-blue-600 border-2">
|
||||
löschen
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}" class=" text-white px-2 py-2 rounded-md text-sm lg:text-lg hover:scale-110 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{% url 'library:copy_quiz' quiz.id %}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75" />
|
||||
</svg>
|
||||
Quiz kopieren
|
||||
</a>
|
||||
|
||||
{% if quiz.questions.count != 0 %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spielen
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<summary class="list-none">
|
||||
<div class="flex items-center gap-3 mt-12">
|
||||
<p class="text-lg font-bold text-blue-600 dark:text-white">Fragen des Quizzes</p>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<details open>
|
||||
{% if quiz.description %}
|
||||
<p class="text-gray-600 mb-4">{{ quiz.description }}</p>
|
||||
{% endif %}
|
||||
<div class="flex justify-end mb-4">
|
||||
{% if detail == True %}
|
||||
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=false">
|
||||
<div class="flex items-center mr-2 dark:text-white">
|
||||
<div class="flex items-center mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
@@ -93,7 +71,7 @@
|
||||
{% else %}
|
||||
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}?show_answers=true">
|
||||
<div class="flex items-center mr-2 dark:text-white">
|
||||
<div class="flex items-center mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
@@ -107,217 +85,93 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow-md border-2 border-blue-600 dark:!bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="border-b border-gray-200 p-4">
|
||||
<div class="flex justify-between items-center flex-wrap">
|
||||
<!-- Titel -->
|
||||
|
||||
<h2 class="text-xl font-semibold">
|
||||
Fragen
|
||||
{% for question in questions %}{% if forloop.last %}({{ forloop.counter }}) {% endif %}{% endfor %}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
<!-- Neue Frage Dropdown -->
|
||||
<h2 class="text-xl font-semibold">Fragen</h2>
|
||||
<div class="flex space-x-2 flex-wrap">
|
||||
{% if quiz.user_id == request.user %}
|
||||
<div class="relative inline-block text-left mt-2 sm:mt-0">
|
||||
<button type="button" class="inline-flex justify-center w-full rounded-md border border-blue-600 shadow-sm px-4 py-2 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" id="menu-button" aria-expanded="true" aria-haspopup="true">
|
||||
Neue Frage
|
||||
<!-- Pfeil nach unten -->
|
||||
<svg class="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.584l3.71-4.354a.75.75 0 011.08 1.04l-4.25 5a.75.75 0 01-1.08 0l-4.25-5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<a href="{% url 'library:new_question' %}?type=input&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-green-100 text-green-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Eingabe
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none hidden group" id="dropdown-menu">
|
||||
<div class="py-1 flex flex-col" role="menu" aria-orientation="vertical" aria-labelledby="menu-button">
|
||||
<a href="{% url 'library:new_question' %}?type=input&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Eingabe</a>
|
||||
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Multiple Choice</a>
|
||||
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Wahr/Falsch</a>
|
||||
<a href="{% url 'library:new_question' %}?type=order&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Reihenfolge</a>
|
||||
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Schätzen</a>
|
||||
<a href="{% url 'library:new_question' %}?type=map&quiz_id={{ quiz.id }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Karte</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Multiple Choice
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Wahr/Falsch
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Toggle Script -->
|
||||
<script>
|
||||
const btn = document.getElementById('menu-button');
|
||||
const menu = document.getElementById('dropdown-menu');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (!btn.contains(e.target) && !menu.contains(e.target)) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
{% if questions %}
|
||||
|
||||
<div class="divide-y divide-gray-200" id="questions-list">
|
||||
<div class="divide-y divide-gray-200">
|
||||
{% for question in questions %}
|
||||
|
||||
|
||||
|
||||
<div data-question-id="{{ question.id }}" class="question-item w-full rounded-xl p-4 hover:bg-gray-50 dark:hover:bg-[#2a2f3a] dark:text-white">
|
||||
|
||||
<div class="w-full rounded-xl p-4 hover:bg-gray-50">
|
||||
{% if quiz.user_id == request.user %} <a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" > {% endif %}
|
||||
<div class="flex justify-between items-start">
|
||||
<p class="question-counter pr-4 ">{{ forloop.counter }}.</p>
|
||||
|
||||
<div class="flex-grow">
|
||||
<p class="font-medium">{{ question.data.question }}</p>
|
||||
<div class="mt-1">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'map' %}bg-orange-100 text-orange-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
|
||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice
|
||||
{% elif question.data.type == 'input' %}Eingabe
|
||||
{% elif question.data.type == 'map' %}Karte
|
||||
{% elif question.data.type == 'true_false' %}Wahr/Falsch
|
||||
{% elif question.data.type == 'order' %}Reihenfolge
|
||||
{% else %}Schätzen
|
||||
{% endif %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% else %}bg-purple-100 text-purple-800{% endif %}">
|
||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% else %}Wahr/Falsch{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% if detail %}
|
||||
|
||||
{% if question.data.type == 'multiple_choice' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
{% for option in question.data.options|dictsort:"order" %}
|
||||
|
||||
|
||||
{% for option in question.data.options %}
|
||||
<li class="flex items-center text-sm">
|
||||
|
||||
{% if option.is_correct %}
|
||||
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="font-medium text-green-700">{{ option.value }}</span>
|
||||
{% else %}
|
||||
<svg class="h-4 w-4 text-gray-400 mr-1.5 dark:text-white " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
|
||||
</svg>
|
||||
<span class="text-gray-600 dark:text-white">{{ option.value }}</span>
|
||||
<span class="text-gray-600">{{ option.value }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif question.data.type == 'map' and detail %}
|
||||
<div class="mt-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
<span class="font-medium">Zielort:</span>
|
||||
<span id="target-location-{{ forloop.counter }}">
|
||||
{% if question.data.target_location %}
|
||||
{{ question.data.target_location }}
|
||||
{% else %}
|
||||
Kein Zielort festgelegt
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if question.data.target_location %}
|
||||
<script>
|
||||
// Format the coordinates to 4 decimal places
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const targetEl = document.getElementById('target-location-{{ forloop.counter }}');
|
||||
if (targetEl) {
|
||||
const coords = targetEl.textContent.trim().split(',');
|
||||
if (coords.length === 2) {
|
||||
const lat = parseFloat(coords[0]).toFixed(4);
|
||||
const lng = parseFloat(coords[1]).toFixed(4);
|
||||
targetEl.textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if question.data.tolerance_radius %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 mt-1">
|
||||
<span class="font-medium">Toleranzradius:</span> {{ question.data.tolerance_radius }} Meter
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif question.data.type == 'order' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
{% for option in question.data.options|dictsort:"order" %}
|
||||
<li class="flex items-center text-sm">
|
||||
{{ forloop.counter }}.
|
||||
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif question.data.type == 'input' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
{% for option in question.data.options %}
|
||||
<li class="flex items-center text-sm">
|
||||
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
|
||||
<span class="font-medium text-green-700">{{ option.value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif question.data.type == 'guess' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
{% for option in question.data.options %}
|
||||
<li class="flex items-center text-sm">
|
||||
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="font-medium text-green-700 dark:text-white">{{ option.value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
{% for option in question.data.options %}
|
||||
{% if option.is_correct %}
|
||||
<p class="text-sm">
|
||||
<span class="text-gray-500 dark:text-white">Richtige Antwort:</span>
|
||||
<span class="ml-1 font-medium {% if option.value == 'Wahr' %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
|
||||
<span class="text-gray-500">Richtige Antwort:</span>
|
||||
<span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -332,7 +186,7 @@
|
||||
<a href=" {% url 'library:delete_question' question.id %}"
|
||||
class=" text-red-700 px-4 py-1 rounded hover:scale-110 ">
|
||||
{% if quiz.user_id == request.user %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="dark:text-white size-7 text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-7 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
@@ -341,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -356,354 +210,32 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
<!-- Kritik zum Quiz -->
|
||||
<div class="max-w-full">
|
||||
{% if quiz.criticism_quiz.all %}
|
||||
|
||||
|
||||
<summary class="list-none">
|
||||
<div class="flex items-center gap-3 mt-12">
|
||||
<p class="text-lg font-bold text-red-800 dark:text-white"> Kritik zum Quiz</p>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-red-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</summary>
|
||||
<details>
|
||||
|
||||
{% for critique in quiz.criticism_quiz.all %}
|
||||
|
||||
|
||||
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-[#2a2f3a] shadow-sm">
|
||||
|
||||
{% if critique.id_question != None %}
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:edit_question' critique.id_question %}" class="text-blue-600 font-bold hover:underline">{{critique.question_criticism}}</a>:
|
||||
{% else %}
|
||||
<span class="font-bold">{{critique.question_criticism}}</span>:
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<span class="dark:text-white dark:bg-transparent">
|
||||
{{ critique.criticism }}
|
||||
</span>
|
||||
<p class="text-sm italic dark:text-white dark:bg-transparent ">
|
||||
Kritik von User {{ critique.user }}
|
||||
</p>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<p class="text-xs italic dark:text-white dark:bg-transparent ">
|
||||
{{ critique.creation_date }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if quiz.user_id == request.user %}
|
||||
<form method="POST" class="space-y-6" enctype="multipart/form-data" action="{% url 'library:delete_critique' critique.id quiz.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="flex border-t-2 border-gray-200 mt-2">
|
||||
<button type="submit" class=" mt-2
|
||||
px-4 py-2 border border-transparent shadow-sm text-sm
|
||||
font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 cursor-pointer
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Fehler von mir behoben!
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<summary class="list-none">
|
||||
<div class="flex items-center gap-3 mt-12">
|
||||
<p class="text-lg font-bold text-blue-600 dark:text-white">Informationen über das Quiz</p>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
|
||||
</summary>
|
||||
<details>
|
||||
|
||||
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
|
||||
Quiz-Infos
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<div class="flex">
|
||||
{% if quiz.description %}
|
||||
<p class="text-gray-600 dark:text-white">Beschreibung: {{ quiz.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
{% if quiz.base_quiz %}
|
||||
<p class="text-gray-600 dark:text-white">Bearbeitung:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></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 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
|
||||
<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 %}
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex justify-end">
|
||||
{% if quiz.base_quiz %}
|
||||
<p class="text-gray-600 dark:text-white ">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 %}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex justify-end">
|
||||
{% if quiz.base_quiz %}
|
||||
<p class="text-gray-600 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></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 %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex">
|
||||
|
||||
<p class="text-gray-600 dark:text-white">Quiz ID:<span class="font-bold"> {{ quiz.pk }}</span></p>
|
||||
|
||||
</div>
|
||||
|
||||
{% if quiz.reference_quizzes.all %}
|
||||
<div class="flex">
|
||||
<p class="text-gray-600 dark:text-white">Verweis auf andere Quizze:
|
||||
<span class="font-bold">
|
||||
{% for reference in quiz.reference_quizzes.all %}
|
||||
<a href="{% url 'library:detail_quiz' reference.id %}">{{ reference.name}}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% empty %}
|
||||
|
||||
{% endfor %}
|
||||
</span></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<div class="flex flex-col max-w-full">
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
|
||||
Credits
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
{% if quiz.credits %}
|
||||
<p class="text-blue-500 mb-2 w-full break-words dark:text-gray-400 ">
|
||||
<span class="break-words italic">{{ quiz.credits }}</span>
|
||||
<span class="font-normal break-words text-gray-600 dark:text-white">(Credits für das Quiz)</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-gray-500 dark:text-gray-400 ">Keine Credits für das Quizformular vorhanden.</p>
|
||||
<p class="text-gray-600 mb-2">Credits:<span class="text-blue-600 font-bold"> {{ quiz.credits }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col max-w-full">
|
||||
<ul class="space-y-2">
|
||||
{% regroup questions by credits as grouped_credits %}
|
||||
{% if grouped_credits|length == 1 and not grouped_credits.0.grouper %}
|
||||
<li class="text-gray-500 dark:text-gray-400 ">Keine zusätzlichen Credits für die einzelnen Fragen vorhanden.</li>
|
||||
{% else %}
|
||||
{% for question in questions %}
|
||||
{% if question.credits %}
|
||||
<li class="text-blue-500 mb-2 w-full break-words break-words dark:text-gray-400 ">
|
||||
<span class="italic">{{ question.credits }}</span>
|
||||
<span class="font-normal break-words text-gray-600 dark:text-white">(Frage: {{ forloop.counter }})</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
|
||||
Quiz-Statistik
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 text-gray-700 dark:text-white ">
|
||||
<p>
|
||||
<span class="font-semibold">{{ quiz.quiz_played_Lernmodus }}</span>
|
||||
mal im Lernmodus gespielt
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span class="font-semibold">{{ quiz.quiz_played_Moderiermodus }}</span>
|
||||
mal im Moderiermodus gespielt
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
(insg. {{ quiz.quiz_played_Moderiermodus_players }} Spieler)
|
||||
</span>
|
||||
</p>
|
||||
<p class="text-xs opacity-50">
|
||||
* zählt nur, wenn das Quiz vollständig beendet wird
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
{% if quiz.user_id != request.user %}
|
||||
|
||||
<summary class="list-none">
|
||||
<div class="flex items-center gap-3 mt-12">
|
||||
<p class="text-lg font-bold text-red-800 dark:text-white">Das Quiz kritisieren</p>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-red-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</summary>
|
||||
<details>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="max-w-full p-4 mt-4 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<p class="text-lg font-bold text-red-800 dark:text-white dark:bg-transparent ">
|
||||
Quiz kritisieren
|
||||
</p>
|
||||
<p class="text-xs opacity-50 mb-3 dark:text-white dark:bg-transparent">
|
||||
Sie haben einen Fehler in dem Quiz entdeckt oder halten eine Frage für nicht angemessen? <br>
|
||||
Dann weisen Sie den Autor gern darauf hin.
|
||||
</p>
|
||||
<select name="question_id" required
|
||||
class="w-full rounded-lg border-gray-300 p-2 dark:bg-gray-800 dark:text-white">
|
||||
<option value="">Kritik an einer bestimmten Frage oder am Quiz insgesamt?</option>
|
||||
|
||||
<option value="whole_quiz">
|
||||
allgemeine Kritik am Quiz {{ quiz.name|truncatechars:60 }}
|
||||
</option>
|
||||
|
||||
{% for question in questions %}
|
||||
<option value="{{ question.id }}">
|
||||
Frage {{ forloop.counter }}: {{ question.data.question|truncatechars:60 }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="critic" required
|
||||
class="w-full rounded-lg border-gray-300 p-2 dark:bg-gray-800 dark:text-white mb-4">
|
||||
<option value="">Was ist Ihre Kritik?</option>
|
||||
|
||||
<option value="content_error">
|
||||
inhaltlicher Fehler
|
||||
</option>
|
||||
|
||||
<option value="spelling_error">
|
||||
Rechtschreibfehler oder Grammatikfehler
|
||||
</option>
|
||||
|
||||
<option value="logic_error">
|
||||
umständliche oder unsinnige Formulierung
|
||||
</option>
|
||||
|
||||
<option value="inappropriate_error">
|
||||
unangemessen
|
||||
</option>
|
||||
<option value="not_completed_error">
|
||||
unvollständig
|
||||
</option>
|
||||
<option value="source_error">
|
||||
Quellen unvollständig
|
||||
</option>
|
||||
<option value="quiz_error">
|
||||
Quiz als "öffentliches Quiz" ungeeignet
|
||||
</option>
|
||||
|
||||
</select>
|
||||
|
||||
<button type="submit" class="inline-flex items-center
|
||||
px-4 py-2 border border-transparent shadow-sm text-sm
|
||||
font-medium rounded-md text-white bg-red-600 hover:bg-red-700
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Abschicken
|
||||
</button>
|
||||
{% elif not user.is_authenticated %}
|
||||
|
||||
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
|
||||
<p class="text-lg font-bold text-red-800 dark:text-white dark:bg-transparent ">
|
||||
Quiz kritisieren
|
||||
</p>
|
||||
<div class="text-xs opacity-50 mb-3 dark:text-white dark:bg-transparent">
|
||||
Sie haben einen Fehler in dem Quiz entdeckt oder halten eine Frage für nicht angemessen? <br>
|
||||
Dann weisen Sie den Autor gern darauf hin.
|
||||
Sie können nur Quizze anderer User kritisieren.
|
||||
</div>
|
||||
<a class="inline-flex items-center
|
||||
px-4 py-2 border border-transparent shadow-sm text-sm
|
||||
font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" href="{% url 'accounts:login' %}">Anmelden</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
{% if quiz.user_id == request.user %}
|
||||
<style>.sortable-drag {
|
||||
position: fixed !important;
|
||||
z-index: 99999 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
Sortable.create(document.getElementById('questions-list'), {
|
||||
animation: 300,
|
||||
swapThreshold: 0.5,
|
||||
|
||||
delay: 100,
|
||||
delayOnTouchOnly: true,
|
||||
touchStartThreshold: 6,
|
||||
|
||||
forceFallback: true,
|
||||
fallbackTolerance: 6,
|
||||
fallbackOnBody: true,
|
||||
|
||||
|
||||
|
||||
onStart: () => {
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(75);
|
||||
}
|
||||
},
|
||||
|
||||
onEnd: () => {
|
||||
|
||||
const items = Array.from(document.querySelectorAll('#questions-list .question-item'));
|
||||
|
||||
// Counter im Browser updaten
|
||||
items.forEach((el, i) => {
|
||||
const counterEl = el.querySelector('.question-counter');
|
||||
if (counterEl) counterEl.textContent = `${i+1}.`;
|
||||
});
|
||||
// Reihenfolge sammeln
|
||||
const order = Array.from(document.querySelectorAll('#questions-list .question-item'))
|
||||
.map((el, i) => ({id: el.dataset.questionId, position: i+1}));
|
||||
|
||||
|
||||
// an Django senden
|
||||
fetch("{% url 'library:reorder_questions' %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({order})
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,97 +1,37 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
{{ form.media }}
|
||||
<style>
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
min-width: 0; /* verhindert Überlaufen bei kleinen Screens */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Das Auswahlfeld */
|
||||
.select2-selection {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Angezeigter Text im Auswahlfeld */
|
||||
.select2-selection__rendered {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select2-selection__choice {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<input type="hidden" name="pk" value="{{ form.instance.pk }}" />
|
||||
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div
|
||||
class="max-w-lg field-group mx-auto bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white"
|
||||
>
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="dark-form space-y-4"
|
||||
>
|
||||
{% csrf_token %} {{ form.as_p }} {% if form.instance.image %}
|
||||
<img
|
||||
src="{{ form.instance.image.image.url }}"
|
||||
style="max-width: 100px"
|
||||
alt="Bild der Frage"
|
||||
/>
|
||||
{% 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" />
|
||||
<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="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent"
|
||||
type="file"
|
||||
name="quiz_image"
|
||||
id="quiz_image"
|
||||
/>
|
||||
<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="text-center text-gray-600 text-sm dislay-flex align-bottom">
|
||||
<label class=""
|
||||
>Mit der Erstellung von diesem Quiz stimmen Sie der
|
||||
<a class="text-blue-600" href="/privacy">Datenschutzerklärung </a>
|
||||
zu.</label
|
||||
>
|
||||
<label class=''>Mit der Erstellung von diesem Quiz stimmen Sie der <a class="text-blue-600" href="/privacy">Datenschutzerklärung </a> zu.</label>
|
||||
</div>
|
||||
|
||||
<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 %}"
|
||||
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"
|
||||
>
|
||||
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,145 +0,0 @@
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
|
||||
|
||||
.custom-outline {
|
||||
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||
<button id="filterButton" class="dark:text-white dark:hover:bg-[#2a2f3a] p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
<span>Filter</span>
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url link %}" class="dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Zurücksetzen
|
||||
</a>
|
||||
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neues Quiz
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filterPanel" class="container mx-auto px-4 {% if request.GET.filter == '1' %}block{% else %}hidden{% endif %}">
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200 dark:bg-transparent dark:text-white ">
|
||||
<form action="{% url link %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<input type="hidden" name="filter" id="filter-input">
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<input type="checkbox" name="use_min_amount_questions" id="use_min_amount_questions" {% if form.use_min_amount_questions.value %}checked{% endif %}>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white">minimale Anzahl an Fragen</label>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
name="min_amount_questions"
|
||||
id="id_min_amount_questions"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ form.min_amount_questions.value|default_if_none:0 }}"
|
||||
class="w-full accent-blue-600 dark:text-white"
|
||||
oninput="document.getElementById('min_amount_value').textContent = this.value + ' Frage(n)'"
|
||||
{% if not form.use_min_amount_questions.value %}disabled{% endif %}
|
||||
>
|
||||
|
||||
<span id="min_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
|
||||
{{ form.min_amount_questions.value|default_if_none:0 }} Frage(n)
|
||||
</span>
|
||||
<br>
|
||||
<span class="text-xs text-gray-600 dark:text-white"> Quizze anderer User werden erst ab min. 1 Frage angezeigt.</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<input type="checkbox" name="use_max_amount_questions" id="use_max_amount_questions" {% if form.use_max_amount_questions.value %}checked{% endif %}>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white">maximale Anzahl an Fragen</label>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
name="max_amount_questions"
|
||||
id="id_max_amount_questions"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ form.max_amount_questions.value|default_if_none:100 }}"
|
||||
class="w-full accent-blue-600"
|
||||
oninput="document.getElementById('max_amount_value').textContent = this.value+' Frage(n)'"
|
||||
{% if not form.use_max_amount_questions.value %}disabled{% endif %}
|
||||
>
|
||||
|
||||
<span id="max_amount_value" class="text-sm font-semibold text-gray-800 dark:text-white">
|
||||
{{ form.max_amount_questions.value|default_if_none:100 }} Frage(n)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white">veröffentlicht von User</label>
|
||||
<input type="text" name="user" class="border-2 border-gray-300 rounded-lg p-2 w-full dark:text-white" id="id_user" value="{{ form.user.value|default_if_none:'' }}">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white">Kategorie</label>
|
||||
{{ form.categories }}
|
||||
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white">Suche</label>
|
||||
{{ form.search }}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="md:col-span-3 flex justify-end">
|
||||
<button
|
||||
type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
||||
</svg>
|
||||
Filtern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
|
||||
const checkbox = document.getElementById('use_min_amount_questions');
|
||||
const slider = document.getElementById('id_min_amount_questions');
|
||||
|
||||
const checkbox_max = document.getElementById('use_max_amount_questions');
|
||||
const slider_max = document.getElementById('id_max_amount_questions');
|
||||
|
||||
checkbox.addEventListener('change', () => {
|
||||
slider.disabled = !checkbox.checked;
|
||||
});
|
||||
|
||||
checkbox_max.addEventListener('change', () => {
|
||||
slider_max.disabled = !checkbox_max.checked;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
|
||||
@@ -13,54 +13,71 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'library/head.html' %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- Alle -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
|
||||
|
||||
|
||||
<div class="flex justify-end text-sm mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
|
||||
<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" />
|
||||
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||
<button id="filterButton" class="p-2 hover:bg-gray-100 rounded-lg transition-colors duration-200 flex items-center gap-2 text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
<div class="gap-2 mx-2">
|
||||
{% if user.is_authenticated %}
|
||||
Anzahl aller Ergebnisse:
|
||||
<span class="font-bold"> {{ total_all_found }}</span>
|
||||
<span>Filter</span>
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'library:overview_quiz' %}" class="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Zurücksetzen
|
||||
</a>
|
||||
<a href="{% url 'library:new_quiz' %}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neues Quiz
|
||||
</a>
|
||||
</div>
|
||||
(verschiedene:
|
||||
<span class="font-bold ml-1"> {{ total_all }}</span>)
|
||||
{% else %}
|
||||
Anzahl aller Ergebnisse:
|
||||
<span class="font-bold"> {{ total_all_found }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
|
||||
<p class="font-black underline">Alle ({{ total_all }})</p>
|
||||
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
|
||||
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
|
||||
<div id="filterPanel" class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<form action="{% url 'library:overview_quiz' %}" method="get" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<input type="hidden" name="filter" value="1">
|
||||
<div class="space-y-2">
|
||||
<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" value="{{ form.min_amout_questions.value|default_if_none:'' }}">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<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" value="{{ form.max_amout_questions.value|default_if_none:'' }}">
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<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" value="{{ form.user.value|default_if_none:'' }}">
|
||||
</div>
|
||||
<div class="md:col-span-3 flex justify-end">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
||||
</svg>
|
||||
Filtern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="all-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Alle</h2>
|
||||
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Eigene Quiz</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if all_quizzes %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
|
||||
{% for quiz in all_quizzes %}
|
||||
<article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden flex flex-col">
|
||||
{% for quiz in my_quizzes %}
|
||||
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
|
||||
<!-- Image Section (Top) -->
|
||||
<div class="h-48 w-full relative flex-shrink-0">
|
||||
{% if quiz.image %}
|
||||
@@ -72,24 +89,23 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if quiz.id not in favorite_quiz_ids %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
@@ -97,28 +113,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
|
||||
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Content Section (Bottom) -->
|
||||
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white" >
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
|
||||
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
|
||||
|
||||
<!-- Quiz Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
@@ -127,7 +133,7 @@
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
|
||||
<span class="text-gray-800">{{ quiz.difficulty }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Questions Count -->
|
||||
@@ -135,7 +141,7 @@
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
|
||||
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
@@ -143,14 +149,9 @@
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
|
||||
<span class="text-gray-800">{{ quiz.status }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="flex items-center gap-1">
|
||||
{% if quiz.rating_count > 0 %}
|
||||
@@ -161,37 +162,26 @@
|
||||
<span class="text-gray-300 text-base">★</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
|
||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
|
||||
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
{% if quiz.user_id %}
|
||||
|
||||
<!-- Authors -->
|
||||
{% if quiz.authors.all %}
|
||||
<div class="flex items-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-600">
|
||||
{{ quiz.user_id.username }}
|
||||
{% for author in quiz.authors.all %}
|
||||
{{ author.username }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
-->
|
||||
|
||||
<!-- Zeit-->
|
||||
{% if quiz.published_at != None %}
|
||||
|
||||
<div class="flex items-center gap-2 ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
|
||||
@@ -209,7 +199,7 @@
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 ">
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
@@ -217,13 +207,13 @@
|
||||
</a>
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
@@ -234,13 +224,14 @@
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pagination für andere Quiz -->
|
||||
{% if all_quizzes.paginator.num_pages > 1 %}
|
||||
<!-- Pagination für eigene Quiz -->
|
||||
{% if my_quizzes.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center space-x-2 mt-6 mb-8">
|
||||
{% if all_quizzes.has_previous %}
|
||||
<a href="?all_page={{ all_quizzes.previous_page_number }}&{{ querystring_all }}"
|
||||
{% if my_quizzes.has_previous %}
|
||||
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
@@ -248,10 +239,384 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ all_quizzes.number }} von {{ all_quizzes.paginator.num_pages }}</span>
|
||||
<span class="px-3 py-1 text-gray-600">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
|
||||
|
||||
{% if all_quizzes.has_next %}
|
||||
<a href="?all_page={{ all_quizzes.next_page_number }}&{{ querystring_all }}"
|
||||
{% if my_quizzes.has_next %}
|
||||
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900">Meine Favoriten</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
|
||||
{% for quiz in favorite_quizzes %}
|
||||
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
|
||||
<!-- Image Section (Top) -->
|
||||
<div class="h-48 w-full relative flex-shrink-0">
|
||||
{% if quiz.image %}
|
||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||
<!-- Hintergrundbild -->
|
||||
<div class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url({{ quiz.image.image.url }});">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if quiz.favorite == False %}
|
||||
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Content Section (Bottom) -->
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
|
||||
|
||||
<!-- Quiz Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<!-- Difficulty -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.difficulty }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Questions Count -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.status }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="flex items-center gap-1">
|
||||
{% if quiz.rating_count > 0 %}
|
||||
{% for i in '12345'|make_list %}
|
||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||
<span class="text-yellow-500 text-base">★</span>
|
||||
{% else %}
|
||||
<span class="text-gray-300 text-base">★</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authors -->
|
||||
{% if quiz.authors.all %}
|
||||
<div class="flex items-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-600">
|
||||
{% for author in quiz.authors.all %}
|
||||
{{ author.username }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
|
||||
{% if quiz.questions.count != 0 %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spielen
|
||||
</a>
|
||||
{% else %}
|
||||
Fürs Spielen ist min. 1 Frage nötig!
|
||||
{% endif %}
|
||||
|
||||
<div class="flex gap-1">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pagination für eigene Quiz -->
|
||||
{% if favorite_quizzes.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center space-x-2 mt-6 mb-8">
|
||||
{% if favorite_quizzes.has_previous %}
|
||||
<a href="?my_page={{ my_quizzes.previous_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
|
||||
|
||||
{% if my_quizzes.has_next %}
|
||||
<a href="?my_page={{ my_quizzes.next_page_number }}{% if request.GET.other_page %}&other_page={{ request.GET.other_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Quiz von anderen Nutzern -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="other-quizzes" class="text-2xl font-bold text-gray-900">Quiz von anderen Nutzern</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if other_quizzes %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
|
||||
{% for quiz in other_quizzes %}
|
||||
<article class="relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
|
||||
<!-- Image Section (Top) -->
|
||||
<div class="h-48 w-full relative flex-shrink-0">
|
||||
{% if quiz.image %}
|
||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||
<!-- Hintergrundbild -->
|
||||
<div class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url({{ quiz.image.image.url }});">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600"></div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if quiz.id not in favorite_quiz_ids %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
<a href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Content Section (Bottom) -->
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3">
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors">{{ quiz.name }}</a>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-gray-700 break-words mb-3">{{ quiz.description }}</p>
|
||||
|
||||
<!-- Quiz Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<!-- Difficulty -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.difficulty }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Questions Count -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.questions.count }} Fragen</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800">{{ quiz.status }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="flex items-center gap-1">
|
||||
{% if quiz.rating_count > 0 %}
|
||||
{% for i in '12345'|make_list %}
|
||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||
<span class="text-yellow-500 text-base">★</span>
|
||||
{% else %}
|
||||
<span class="text-gray-300 text-base">★</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="text-gray-600 text-sm">({{ quiz.rating_count }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600 text-sm">Noch keine Bewertungen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authors -->
|
||||
{% if quiz.authors.all %}
|
||||
<div class="flex items-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-600">
|
||||
{% for author in quiz.authors.all %}
|
||||
{{ author.username }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
|
||||
{% if quiz.questions.count != 0 %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spielen
|
||||
</a>
|
||||
{% else %}
|
||||
Fürs Spielen ist min. 1 Frage nötig!
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination für andere Quiz -->
|
||||
{% if other_quizzes.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center space-x-2 mt-6 mb-8">
|
||||
{% if other_quizzes.has_previous %}
|
||||
<a href="?other_page={{ other_quizzes.previous_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-1 text-gray-600">Seite {{ other_quizzes.number }} von {{ other_quizzes.paginator.num_pages }}</span>
|
||||
|
||||
{% if other_quizzes.has_next %}
|
||||
<a href="?other_page={{ other_quizzes.next_page_number }}{% if request.GET.my_page %}&my_page={{ request.GET.my_page }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
@@ -263,56 +628,44 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<p class="text-gray-600 text-center dark:text-white">Keine Quiz gefunden.</p>
|
||||
<p class="text-gray-600 text-center">Keine öffentlichen Quiz gefunden.</p>
|
||||
</div>
|
||||
{% endif %}<script>
|
||||
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const filterPanel = document.getElementById('filterPanel');
|
||||
const filterButton = document.getElementById('filterButton');
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('filter') === '1') {
|
||||
filterPanel.classList.add('show');
|
||||
document.getElementById('filterPanel').classList.add('show');
|
||||
}
|
||||
|
||||
filterButton.addEventListener('click', function () {
|
||||
filterPanel.classList.toggle('show');
|
||||
document.getElementById('filterButton').addEventListener('click', function() {
|
||||
document.getElementById('filterPanel').classList.toggle('show');
|
||||
|
||||
// URL beim Öffnen aktualisieren
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (filterPanel.classList.contains('show')) {
|
||||
//params.set('filter', '1');
|
||||
if (document.getElementById('filterPanel').classList.contains('show')) {
|
||||
params.set('filter', '1');
|
||||
} else {
|
||||
params.delete('filter');
|
||||
}
|
||||
|
||||
history.replaceState(null, '', `${location.pathname}?${params}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (window.location.pathname === "/library/") {
|
||||
window.addEventListener("beforeunload", function () {
|
||||
localStorage.setItem("meine_seite_scroll", window.scrollY);
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Scroll-Position beim Laden setzen
|
||||
const scrollPos = localStorage.getItem("scroll-position");
|
||||
window.addEventListener("load", function () {
|
||||
const scrollPos = localStorage.getItem("meine_seite_scroll");
|
||||
if (scrollPos !== null) {
|
||||
window.scrollTo(0, parseInt(scrollPos));
|
||||
localStorage.removeItem("scroll-position");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Nur Favoriten-Links abfangen
|
||||
const favoriteLinks = document.querySelectorAll('a.favorite-link');
|
||||
favoriteLinks.forEach(link => {
|
||||
link.addEventListener('click', function () {
|
||||
localStorage.setItem("scroll-position", window.scrollY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
|
||||
|
||||
.custom-outline {
|
||||
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% block head %}
|
||||
{% include 'library/head.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
|
||||
|
||||
|
||||
<div class="flex justify-end text-sm mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
|
||||
<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>
|
||||
<div class="gap-2 mx-2 ">
|
||||
Anzahl aller Ergebnisse:
|
||||
<span class="font-bold"> {{ total_all_found }}</span>
|
||||
</div>
|
||||
(verschiedene:
|
||||
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
|
||||
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
|
||||
<a href="{{ url_my }}?{{ request.GET.urlencode }}">Meine Quizze ({{ total_my }})</a>
|
||||
<p class="font-black underline">Meine Favoriten ({{ total_favorite }})</p>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Favoriten</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if request.user.is_authenticated and favorite_quizzes %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
|
||||
{% for quiz in favorite_quizzes %}
|
||||
<article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col ">
|
||||
<!-- Image Section (Top) -->
|
||||
<div class="h-48 w-full relative flex-shrink-0 ">
|
||||
{% if quiz.image %}
|
||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||
<!-- Hintergrundbild -->
|
||||
<div class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url({{ quiz.image.image.url }});">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if quiz.favorite == False %}
|
||||
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
|
||||
|
||||
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Content Section (Bottom) -->
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
|
||||
|
||||
<!-- Quiz Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<!-- Difficulty -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Questions Count -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="flex items-center gap-1">
|
||||
{% if quiz.rating_count > 0 %}
|
||||
{% for i in '12345'|make_list %}
|
||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||
<span class="text-yellow-500 text-base">★</span>
|
||||
{% else %}
|
||||
<span class="text-gray-300 text-base">★</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authors -->
|
||||
<!--
|
||||
{% if quiz.authors.all %}
|
||||
<div class="flex items-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-600">
|
||||
{% for author in quiz.authors.all %}
|
||||
{{ author.username }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}-->
|
||||
|
||||
<!-- Zeit-->
|
||||
{% if quiz.published_at != None %}
|
||||
|
||||
<div class="flex items-center gap-2 ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 text-sm dark:text-white "><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
|
||||
{% if quiz.questions.count != 0 %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spielen
|
||||
</a>
|
||||
{% else %}
|
||||
Fürs Spielen ist min. 1 Frage nötig!
|
||||
{% endif %}
|
||||
|
||||
<div class="flex gap-1">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pagination für favorite Quizze -->
|
||||
{% if favorite_quizzes.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center space-x-2 mt-6 mb-8 ">
|
||||
{% if favorite_quizzes.has_previous %}
|
||||
<a href="?favorite_page={{ favorite.previous_page_number }}&{{ querystring_favorite }}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="dark:text-white px-3 py-1 text-gray-600">Seite {{ favorite_quizzes.number }} von {{ favorite_quizzes.paginator.num_pages }}</span>
|
||||
|
||||
{% if favorite_quizzes.has_next %}
|
||||
<a href="?favorite_page={{ favorite_quizzes.next_page_number }}&{{ querystring_favorite }}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<p class="text-gray-600 text-center dark:text-white">Keine favorisierten Quiz gefunden.</p>
|
||||
</div>
|
||||
{% endif %}<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const filterPanel = document.getElementById('filterPanel');
|
||||
const filterButton = document.getElementById('filterButton');
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('filter') === '1') {
|
||||
filterPanel.classList.add('show');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
filterButton.addEventListener('click', function () {
|
||||
filterPanel.classList.toggle('show');
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (filterPanel.classList.contains('show')) {
|
||||
//params.set('filter', '1');
|
||||
} else {
|
||||
params.delete('filter');
|
||||
}
|
||||
|
||||
history.replaceState(null, '', `${location.pathname}?${params}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Scroll-Position beim Laden setzen
|
||||
const scrollPos = localStorage.getItem("scroll-position");
|
||||
if (scrollPos !== null) {
|
||||
window.scrollTo(0, parseInt(scrollPos));
|
||||
localStorage.removeItem("scroll-position");
|
||||
}
|
||||
|
||||
// Nur Favoriten-Links abfangen
|
||||
const favoriteLinks = document.querySelectorAll('a.favorite-link');
|
||||
favoriteLinks.forEach(link => {
|
||||
link.addEventListener('click', function () {
|
||||
localStorage.setItem("scroll-position", window.scrollY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,306 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
|
||||
|
||||
.custom-outline {
|
||||
-webkit-text-stroke: 0.2px rgb(0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'library/head.html' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-6 dark:text-white">
|
||||
|
||||
<div class="flex justify-end text-sm mb-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 text-blue-600">
|
||||
<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>
|
||||
<div class="gap-2 mx-2">
|
||||
Anzahl aller Ergebnisse:
|
||||
<span class="font-bold"> {{ total_all_found }}</span>
|
||||
</div>
|
||||
(verschiedene:
|
||||
<span class="font-bold ml-1"> {{ total_all }}</span>) </div>
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-4 font-bold text-blue-600 whitespace-nowrap text-sm mb-2 md:mb-0">
|
||||
<a href="{{ url_all }}?{{ request.GET.urlencode }}">Alle ({{ total_all }})</a>
|
||||
<p class="font-black underline">Meine Quizze ({{ total_my }})</p>
|
||||
<a href="{{ url_favorite }}?{{ request.GET.urlencode }}">Meine Favoriten ({{ total_favorite }})</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="my-quizzes" class="text-2xl font-bold text-gray-900 dark:text-white">Meine Quizze</h2>
|
||||
<div class="h-0.5 flex-grow bg-gradient-to-r from-blue-600 to-transparent rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.is_authenticated and my_quizzes %}
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 auto-rows-fr">
|
||||
{% for quiz in my_quizzes %}
|
||||
<article class="{% if quiz in critisized_quizzes %} border-4 border-red-600 {% endif %} relative min-h-[20rem] rounded-xl shadow-lg overflow-hidden bg-white flex flex-col">
|
||||
<!-- Image Section (Top) -->
|
||||
<div class="h-48 w-full relative flex-shrink-0">
|
||||
{% if quiz.image %}
|
||||
<div class="relative w-full h-full inset-0 bg-cover bg-center">
|
||||
<!-- Hintergrundbild -->
|
||||
<div class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url({{ quiz.image.image.url }});">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 flex items-center justify-center font-black text-white text-3xl px-4 overflow-auto break-words hyphens-auto ">{{ quiz.name }}</div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{% if quiz.id not in favorite_quiz_ids %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
|
||||
|
||||
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-black size-6 dark:text-white">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="absolute top-4 right-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg dark:bg-[#2a2f3a]">
|
||||
<a class="favorite-link" href="{% url 'library:favorite_quiz' quiz.pk %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="text-yellow-400 size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="dark:bg-[#2a2f3a] max-w-6/10 h-max-12 truncate absolute top-4 left-4 bg-white bg-opacity-60 text-white px-4 py-2 rounded-lg shadow-lg ">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full w-4 h-4 flex-shrink-0" style="background-color: {{ quiz.category.color }};"></span>
|
||||
<a class=" text-gray-800 truncate text-sm dark:text-white" title="{{ quiz.category }}" href="{% url 'library:overview_quiz' %}?filter=1&categories={{ quiz.category.id }}">{{ quiz.category }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Content Section (Bottom) -->
|
||||
<div class="flex-grow bg-white p-4 flex flex-col gap-3 dark:bg-[#2a2f3a] dark:text-white">
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-gray-900 break-words mb-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="hover:text-blue-600 transition-colors dark:text-white">{{ quiz.name }}</a>
|
||||
</h2>
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-gray-700 break-words mb-3 dark:text-white">{{ quiz.description }}</p>
|
||||
|
||||
<!-- Quiz Info Grid -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<!-- Difficulty -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.difficulty }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Questions Count -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.questions.count }} Fragen</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 dark:text-white">{{ quiz.status }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div class="flex items-center gap-1">
|
||||
{% if quiz.rating_count > 0 %}
|
||||
{% for i in '12345'|make_list %}
|
||||
{% if forloop.counter <= quiz.average_rating|floatformat:0|add:0 %}
|
||||
<span class="text-yellow-500 text-base">★</span>
|
||||
{% else %}
|
||||
<span class="text-gray-300 text-base">★</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">({{ quiz.rating_count }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600 text-sm dark:text-white">Noch keine Bewertungen</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authors -->
|
||||
<!--
|
||||
{% if quiz.authors.all %}
|
||||
<div class="flex items-center gap-2 text-sm mt-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-gray-600">
|
||||
{% for author in quiz.authors.all %}
|
||||
{{ author.username }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
-->
|
||||
|
||||
|
||||
<!-- Zeit-->
|
||||
{% if quiz.published_at != None %}
|
||||
|
||||
<div class="flex items-center gap-2 ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="text-green-400 size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
<span class="text-gray-800 text-sm dark:text-white"><span class="font-medium dark:text-white">{{ quiz.published_at |naturaltime }}</span> <span class="text-sm">erst. veröffentlicht</span> </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center gap-2 mt-3 border-t pt-3">
|
||||
{% if quiz.questions.count != 0 %}
|
||||
<a href="{% url 'play:create_game' quiz.id %}"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Spielen
|
||||
</a>
|
||||
{% else %}
|
||||
Fürs Spielen ist min. 1 Frage nötig!
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</a>
|
||||
{% if quiz.user_id == request.user %}
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}"
|
||||
class="p-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-gray-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pagination für Meine Quizze -->
|
||||
{% if my_quizzes.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center space-x-2 mt-6 mb-8">
|
||||
{% if my_quizzes.has_previous %}
|
||||
<a href="?my_page={{ my_quizzes.previous_page_number }}&{{ querystring_my }}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-1 text-gray-600 dark:text-white">Seite {{ my_quizzes.number }} von {{ my_quizzes.paginator.num_pages }}</span>
|
||||
|
||||
{% if my_quizzes.has_next %}
|
||||
<a href="?my_page={{ my_quizzes.next_page_number }}&{{ querystring_my }}"
|
||||
class="px-3 py-1 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<p class="text-gray-600 text-center dark:text-white">Keine eigenen Quiz gefunden.</p>
|
||||
</div>
|
||||
{% endif %}<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const filterPanel = document.getElementById('filterPanel');
|
||||
const filterButton = document.getElementById('filterButton');
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('filter') === '1') {
|
||||
filterPanel.classList.add('show');
|
||||
}
|
||||
|
||||
filterButton.addEventListener('click', function () {
|
||||
filterPanel.classList.toggle('show');
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (filterPanel.classList.contains('show')) {
|
||||
//params.set('filter', '1');
|
||||
} else {
|
||||
params.delete('filter');
|
||||
}
|
||||
|
||||
history.replaceState(null, '', `${location.pathname}?${params}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Scroll-Position beim Laden setzen
|
||||
const scrollPos = localStorage.getItem("scroll-position");
|
||||
if (scrollPos !== null) {
|
||||
window.scrollTo(0, parseInt(scrollPos));
|
||||
localStorage.removeItem("scroll-position");
|
||||
}
|
||||
|
||||
// Nur Favoriten-Links abfangen
|
||||
const favoriteLinks = document.querySelectorAll('a.favorite-link');
|
||||
favoriteLinks.forEach(link => {
|
||||
link.addEventListener('click', function () {
|
||||
localStorage.setItem("scroll-position", window.scrollY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,150 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Schätzfrage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if image %}
|
||||
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||
<label for="reset_image">
|
||||
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||
Bild zurücksetzen
|
||||
</label>
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<!-- Eingabe -->
|
||||
{% if question and question.data.options %}
|
||||
{% for option in question.data.options %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="correct_answer" type="number" value="{{ question.data.correct_answer|default:'' }}"
|
||||
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="min" type="number" value="{{ question.data.min|default:'' }}"
|
||||
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="max" type="number" value="{{ question.data.max|default:'' }}"
|
||||
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="tolerance" type="number" value="{{ question.data.tolerance|default:'' }}" max="100" min="1" value="75"
|
||||
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="correct_answer" type="number"
|
||||
class="p-3 dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Minimale Zahlenauswahl</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="min" type="number" value="0"
|
||||
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Maximale Zahlenauswahl</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="max" type="number"
|
||||
class="p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Punkte ab Genauigkeit von ... (in %)</label>
|
||||
<div class="flex space-x-4">
|
||||
<input name="tolerance" type="number" max="100" min="1" value="75"
|
||||
class=" p-3 dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></input>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
|
||||
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,23 +3,23 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Eingabe Frage{% endif %}</h1>
|
||||
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Eingabe Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,17 +33,17 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<!-- Eingabe -->
|
||||
{% if question and question.data.options %}
|
||||
{% for option in question.data.options %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<textarea name="correct_answer" rows="2"
|
||||
class="dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ option.value }}</textarea>
|
||||
|
||||
</div>
|
||||
@@ -52,10 +52,10 @@
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<textarea name="correct_answer" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<select name="time_per_question" id="time_per_question">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
@@ -75,12 +75,6 @@
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div id="mapquestion-container">
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Karten Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if image %}
|
||||
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||
<label for="reset_image">
|
||||
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||
Bild zurücksetzen
|
||||
</label>
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<label for="map" class="block text-sm font-medium text-gray-700 dark:text-white">Ziel auswählen</label>
|
||||
<div id="map"></div>
|
||||
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-white">Koordinaten</label>
|
||||
<div class="mt-1">
|
||||
<textarea id="location" name="location" rows="2" required
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ question.data.target_location }}</textarea>
|
||||
</div>
|
||||
|
||||
<label for="tolerance_radius" class="block text-sm font-medium text-gray-700 dark:text-white">Radius (Meter) für volle Punktzahl</label>
|
||||
<div class="mt-1">
|
||||
<textarea id="tolerance_radius" name="tolerance_radius" rows="2" required
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ question.data.tolerance_radius }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
|
||||
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{% if question %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let marker = null;
|
||||
let circle = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
const radiusInput = document.getElementById('tolerance_radius');
|
||||
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateCircle(latlng) {
|
||||
const radius = parseInt(radiusInput.value) || 100;
|
||||
if (circle) {
|
||||
map.removeLayer(circle);
|
||||
}
|
||||
circle = L.circle(latlng, {
|
||||
color: 'blue',
|
||||
fillColor: '#87CEEB',
|
||||
fillOpacity: 0.2,
|
||||
radius: radius
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
if (marker) {
|
||||
updateCircle(marker.getLatLng());
|
||||
}
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
// Update circle when radius changes
|
||||
radiusInput.addEventListener('input', () => {
|
||||
if (marker) {
|
||||
updateCircle(marker.getLatLng());
|
||||
}
|
||||
});
|
||||
|
||||
// Place marker at existing coordinates if available
|
||||
const existingCoords = '{{ question.data.target_location }}';
|
||||
if (existingCoords) {
|
||||
const [lat, lng] = existingCoords.split(',').map(Number);
|
||||
placeOrMoveMarker({ lat, lng });
|
||||
map.setView([lat, lng], 6); // Zoom in to show the marker
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,22 +3,22 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1>
|
||||
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question|default:'' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,24 +33,24 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input value="image.url" class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
<input value="image.url" class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Antwortmöglichkeiten</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Antwortmöglichkeiten</label>
|
||||
<div id="options-container" class="space-y-3">
|
||||
{% if question and question.data.options %}
|
||||
{% for option in question.data.options|dictsort:"order" %}
|
||||
{% for option in question.data.options %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_{{ forloop.counter }}" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ option.value }}</textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_{{ forloop.counter }}" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
{% if option.is_correct %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white dark:text-white">Richtig</span>
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
@@ -63,12 +63,12 @@
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_1" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_1" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
@@ -79,44 +79,12 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_2" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_2" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_3" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_3" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_4" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_4" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
@@ -138,7 +106,7 @@
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<select name="time_per_question" id="time_per_question">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
@@ -147,12 +115,6 @@
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
@@ -183,13 +145,12 @@ function addOption() {
|
||||
div.className = 'flex items-center space-x-3';
|
||||
div.innerHTML = `
|
||||
<textarea name="option_${optionCount}" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ option.value }}</textarea>
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_${optionCount}" value="1"
|
||||
class=" focus:outline-none focus:ring-2 focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
{% if option.is_correct %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Richtig</span>
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Reihenfolge Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question|default:'' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if image %}
|
||||
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||
<label for="reset_image">
|
||||
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||
Bild zurücksetzen
|
||||
</label>
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input value="image.url" class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">richtige Reihenfolge</label>
|
||||
<div id="options-container" class="space-y-3">
|
||||
{% if question and question.data.options %}
|
||||
{% for option in question.data.options|dictsort:"order" %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="number bg-blue-500 p-2 rounded-lg text-white">{{ forloop.counter }}.</span>
|
||||
<textarea name="option_{{ forloop.counter }}" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ option.value }}</textarea>
|
||||
<label class="inline-flex items-center">
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for i in "1234" %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="number bg-blue-500 p-2 rounded-lg text-white">{{ forloop.counter }}.</span>
|
||||
<textarea name="option_{{ forloop.counter }}" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button" onclick="addOption()"
|
||||
class="mt-3 inline-flex items-center px-3 py-1.5 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">
|
||||
<svg class="h-4 w-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Option hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
|
||||
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{% if question %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
function addOption() {
|
||||
const container = document.getElementById('options-container');
|
||||
const optionCount = container.children.length + 1;
|
||||
|
||||
if (optionCount > 6) {
|
||||
alert('Maximal 6 Optionen sind erlaubt.');
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center space-x-3';
|
||||
div.innerHTML = `
|
||||
|
||||
|
||||
|
||||
<span class="number bg-blue-500 p-2 rounded-lg text-white">${optionCount}.</span>
|
||||
<textarea name="option_${optionCount}" rows="2"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
`;
|
||||
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeOption(button) {
|
||||
|
||||
const container = document.getElementById('options-container');
|
||||
if (container.children.length > 2) {
|
||||
button.parentElement.remove();
|
||||
updateOptionNumbers();
|
||||
} else {
|
||||
alert('Mindestens zwei Optionen müssen vorhanden sein.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptionNumbers() {
|
||||
const container = document.getElementById('options-container');
|
||||
const options = container.children;
|
||||
Array.from(options).forEach((option, index) => {
|
||||
const number = index + 1;
|
||||
const textInput = option.querySelector('textarea');
|
||||
|
||||
|
||||
|
||||
textInput.name = `option_${number}`;
|
||||
const numberSpan = option.querySelector('span.number');
|
||||
if (numberSpan) {
|
||||
numberSpan.textContent = `${number}.`;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,23 +3,23 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1>
|
||||
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,31 +33,31 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
<input class="bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<!-- True/False Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 dark:text-white">Richtige Antwort</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="correct_answer" value="true"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
|
||||
{% if question.data.correct_answer %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Wahr</span>
|
||||
<span class="ml-2 text-sm text-gray-600">Wahr</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="correct_answer" value="false"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
|
||||
{% if not question.data.correct_answer %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-white">Falsch</span>
|
||||
<span class="ml-2 text-sm text-gray-600">Falsch</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<select name="time_per_question" id="time_per_question">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
@@ -67,13 +67,6 @@
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
|
||||
@@ -1,48 +1,7 @@
|
||||
<footer class="w-full bg-blue-600 text-white shadow-xl py-6 mt-24 break-words">
|
||||
|
||||
|
||||
|
||||
<div class="flex items-center justify-center flex-col sm:flex-row sm:gap-32">
|
||||
|
||||
<div class="mb-2 sm:m-0">
|
||||
<h1 class="flex justify-center text-md font-bold font-sans tracking-wider gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0 0 12 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.988 5.988 0 0 1-2.031.352 5.988 5.988 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L18.75 4.971Zm-16.5.52c.99-.203 1.99-.377 3-.52m0 0 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.989 5.989 0 0 1-2.031.352 5.989 5.989 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L5.25 4.971Z" />
|
||||
</svg>
|
||||
Rechtliches:</h1>
|
||||
|
||||
<div class="left-0 w-full p-2 flex justify-center space-x-2 text-white">
|
||||
<p class="text-xs tracking-wide"><a class= "hover:underline" href="/impress">Impressum</a></p>
|
||||
<p class="text-xs tracking-wide"><a class= "hover:underline" href="/privacy">Datenschutz</a></p>
|
||||
<p class="text-xs tracking-wide"><a class= "hover:underline" href="https://edugit.org/enrichment-2024/qivip">Repository</a></p>
|
||||
<div class="grid place-content-center gap-2 w-full mt-4">
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<p class="text-sm font-extralight"><a href="/impress">Impressum</a></p>
|
||||
<p class="text-sm font-extralight"><a href="/privacy">Datenschutz</a></p>
|
||||
<p class="text-sm font-extralight"><a href="https://edugit.org/enrichment-2024/qivip">Repository</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="flex justify-center text-md font-bold font-sans tracking-widerd gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" />
|
||||
</svg>
|
||||
|
||||
Erfahren Sie mehr:</h2>
|
||||
<div class="left-0 w-full p-2 flex justify-center space-x-2 text-white">
|
||||
<p class="text-xs tracking-wide"><a class= "hover:underline" href="https://katharineum.de/qivip-unsere-neue-quiz-software/">
|
||||
|
||||
mehr über uns</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="h-0.25 w-200 bg-white opacity-40 rounded-full mt-2">
|
||||
</div></div>
|
||||
|
||||
|
||||
<p class="text-xs text-center opacity-80 mt-2">
|
||||
|
||||
<a class="font-bold" href="/">qivip</a> - Ein Projekt entstanden während des Enrichment-Programms.
|
||||
</p>
|
||||
|
||||
</footer>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
<!-- 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 link|default:'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 dark:bg-gray-900">
|
||||
<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 id="quiz-search-desktop" list="quiz-names-desktop" 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 dark:text-white">
|
||||
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" />
|
||||
@@ -51,10 +51,10 @@
|
||||
<div id="mobile-menu" class="hidden absolute w-full z-50 md:hidden opacity-0 transform -translate-y-2 transition-all duration-300">
|
||||
<div class="bg-blue-500 pt-3 pb-4 rounded-b-lg shadow-inner">
|
||||
<!-- Search in mobile menu -->
|
||||
<form method="get" action="{% url link|default:'library:overview_quiz' %}" class="px-4 mb-4 ">
|
||||
<div class="flex items-center justify-center bg-white rounded-full px-4 py-2 shadow-md dark:bg-gray-900">
|
||||
<form method="get" action="{% url 'library:overview_quiz' %}" class="px-4 mb-4">
|
||||
<div class="flex items-center justify-center bg-white rounded-full px-4 py-2 shadow-md">
|
||||
<input id="quiz-search-mobile" list="quiz-names-mobile" type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
|
||||
class="w-full text-sm px-2 text-black bg-transparent focus:outline-none dark:text-white">
|
||||
class="w-full text-sm px-2 text-black bg-transparent focus:outline-none">
|
||||
<button class="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">
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow-md p-8 mb-6 dark:bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md p-8 mb-6">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-blue-600 mb-4">Quiz beendet!</h1>
|
||||
<p class="text-xl text-gray-600 dark:text-white">Vielen Dank fürs Spielen!</p>
|
||||
<p class="text-xl text-gray-600">Vielen Dank fürs Spielen!</p>
|
||||
</div>
|
||||
|
||||
{% if not is_host %}
|
||||
{% if rating_enabled %}
|
||||
<div class="bg-blue-50 rounded-lg p-6 mb-8 dark:bg-transparent ">
|
||||
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center dark:text-white">Wie hat dir das Quiz gefallen?</h2>
|
||||
<div class="bg-blue-50 rounded-lg p-6 mb-8">
|
||||
<h2 class="text-2xl font-bold text-blue-800 mb-4 text-center">Wie hat dir das Quiz gefallen?</h2>
|
||||
<div class="stars flex justify-center space-x-4 mb-4">
|
||||
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="1"></i>
|
||||
<i class="fas fa-star text-3xl transition-all duration-200 hover:scale-110" data-rating="2"></i>
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
|
||||
|
||||
{% if request.session.mode == "learn" %}
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-end mb-4">
|
||||
{% if request.session.mode == "learn" %}
|
||||
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
Spiel verlassen
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="this.onclick=null; advanceToScores();" class="mx-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
|
||||
überspringen
|
||||
</button>
|
||||
</div>
|
||||
<div id="form_host" class="bg-white rounded-lg shadow-md p-4 mb-6 dark:bg-transparent dark:text-white">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="flex gap-1 text-sm"><p class="text-sm">Frage</p>
|
||||
<p class="text-gray-700 font-bold text-sm dark:text-white">{{ question_index|add:1 }}</p>
|
||||
<p class="text-sm">von</p>
|
||||
<p class="text-gray-700 font-bold text-sm dark:text-white">{{ total_questions }}</p>
|
||||
</div>
|
||||
<div class="flex gap-1 mb-2"><p class="text-sm">Antworten:</p> <p id="answer-count" class="text-gray-700 font-bold text-sm dark:text-white"></p></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 class="text-xl font-black mb-2 border-blue-500 border-4 p-4 text-center rounded-lg md:text-2xl lg:text-4xl">{{ question_data.question}}</h1>
|
||||
</div>{% endif %}
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
|
||||
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
|
||||
|
||||
{% if question_image %}
|
||||
<div class="flex justify-center">
|
||||
<img class="m-4 max-h-[33vh] w-auto rounded-lg" src="{{ question_image.image.url }}" alt="Bild der Frage">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.session.mode != "learn" %}
|
||||
<div class="w-full bg-gray-300 rounded-full h-4">
|
||||
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
|
||||
<img class="m-4 w-full max-w-[500px] " src="{{ question_image.image.url }}" alt="Bild der Frage">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -47,237 +24,47 @@
|
||||
{% else %}
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if request.session.mode != "learn" %}
|
||||
<div class="p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit </p>
|
||||
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
|
||||
</div>
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-blue-600 text-xl mb-2">Antworten</p>
|
||||
<p id="answer-count" class="text-6xl font-bold text-blue-700">0/0</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if question_data.type != "input" %}
|
||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||
{% for option in question_data.options %}
|
||||
<button
|
||||
class="
|
||||
{% if forloop.counter0 == 0 %}
|
||||
bg-green-500
|
||||
hover:bg-green-600
|
||||
{% elif forloop.counter0 == 1 %}
|
||||
bg-red-500
|
||||
hover:bg-red-600
|
||||
{% elif forloop.counter0 == 2 %}
|
||||
bg-blue-500
|
||||
hover:bg-blue-600
|
||||
{% elif forloop.counter0 == 3 %}
|
||||
bg-purple-500
|
||||
hover:bg-purple-600
|
||||
{% elif forloop.counter0 == 4 %}
|
||||
bg-orange-500
|
||||
hover:bg-orange-600
|
||||
{% elif forloop.counter0 == 5 %}
|
||||
bg-yellow-500
|
||||
hover:bg-yellow-600
|
||||
{% else %}
|
||||
bg-gray-500
|
||||
hover:bg-gray-600
|
||||
{% endif %}
|
||||
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
|
||||
class="p-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"
|
||||
data-index="{{ forloop.counter0 }}"
|
||||
{% if request.session.mode == "learn" %}
|
||||
onclick="submitAnswer({{ forloop.counter0 }});" {% endif %}>
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
</button>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% elif question_data.type == "order" %}
|
||||
|
||||
<div id="sortable-options" class=" mb-4 flex flex-col items-center">
|
||||
{% for option in question_data.options %}
|
||||
|
||||
<button data-index="{{ forloop.counter0 }}"
|
||||
data-value="{{ option.value }}"
|
||||
|
||||
class=" my-2 w-3/4 max-w-xl
|
||||
{% if forloop.counter0 == 0 %}
|
||||
bg-green-500
|
||||
hover:bg-green-600
|
||||
{% elif forloop.counter0 == 1 %}
|
||||
bg-red-500
|
||||
hover:bg-red-600
|
||||
{% elif forloop.counter0 == 2 %}
|
||||
bg-blue-500
|
||||
hover:bg-blue-600
|
||||
{% elif forloop.counter0 == 3 %}
|
||||
bg-purple-500
|
||||
hover:bg-purple-600
|
||||
{% elif forloop.counter0 == 4 %}
|
||||
bg-orange-500
|
||||
hover:bg-orange-600
|
||||
{% elif forloop.counter0 == 5 %}
|
||||
bg-yellow-500
|
||||
hover:bg-yellow-600
|
||||
{% else %}
|
||||
bg-gray-500
|
||||
hover:bg-gray-600
|
||||
{% endif %}
|
||||
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
|
||||
data-index="{{ forloop.counter0 }}"
|
||||
|
||||
>
|
||||
|
||||
<p class="text-lg flex items-center">
|
||||
<span class="order-number inline-block w-8 h-8 rounded-full bg-white text-gray-800 font-bold text-center leading-8 mr-3">1</span>
|
||||
|
||||
{{ option.value }}
|
||||
</p>
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{% if request.session.mode == "learn" %}
|
||||
<button
|
||||
onclick="submitOrderAnswer()"
|
||||
id="send_order" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% elif question_data.type == "input" %}
|
||||
{% if request.session.mode == "learn" %}
|
||||
<form action="">
|
||||
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
|
||||
class="answer-option text-center mb-4 p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a] ">
|
||||
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 ">
|
||||
|
||||
<button
|
||||
onclick="submitAnswer( -1 );"
|
||||
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
<button {% if request.session.mode == "learn" %}
|
||||
onclick="submitAnswer( -1 );" {% endif %}
|
||||
class="mt-2 text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option">
|
||||
abschicken
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
{% elif question_data.type == "map" and request.session.mode == "learn" %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||
<div id="map" class="h-96"></div>
|
||||
<input type="hidden" value="" id="singleanswer" name="singleanswer">
|
||||
<div class="mt-4">
|
||||
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||
Abgeben
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="location" name="location" value="">
|
||||
<script>
|
||||
let marker = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
|
||||
function submitAnswerMap() {
|
||||
const answer = document.getElementById("location").value;
|
||||
|
||||
if (answer === "") {
|
||||
alert("Bitte einen Ort auf der Karte auswählen!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add grey overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||
overlay.style.zIndex = '1000';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.color = 'white';
|
||||
overlay.style.fontSize = '24px';
|
||||
overlay.style.fontWeight = 'bold';
|
||||
overlay.textContent = 'Antwort übermittelt';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const now = Date.now();
|
||||
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
|
||||
// Submit the answer
|
||||
submitAnswer(answer);
|
||||
}
|
||||
</script>
|
||||
{% elif question_data.type == "map" and request.session.mode != "learn" %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2 text-center">Bitte den gesuchten Ort auf der Karte auswählen</p>
|
||||
{% elif question_data.type == "guess" %}
|
||||
{% if request.session.mode == "learn" %}
|
||||
<form class="flex flex-col items-center gap-4 mt-4">
|
||||
<!-- Slider mit Beschriftung -->
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<input
|
||||
id="textanswer"
|
||||
type="range"
|
||||
min="{{ question_data.min }}"
|
||||
max="{{ question_data.max }}"
|
||||
value="{{ question_data.min }}"
|
||||
step="1"
|
||||
oninput="document.getElementById('range-value').textContent = 'Wert: ' + this.value;"
|
||||
class="w-full answer-option bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]"
|
||||
>
|
||||
<p id="range-value" class="mt-2 font-bold text-gray-800 dark:text-white">Wert: {{ question_data.min }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Abschicken-Button -->
|
||||
|
||||
<button
|
||||
onclick="submitAnswer(-1);"
|
||||
|
||||
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
</button>
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -309,12 +96,7 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
} else if (data.type === 'game_state_update' &&
|
||||
data.action === 'show_scores' &&
|
||||
data.redirect_url) {
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
submitAnswer(-1);
|
||||
window.location.href = data.redirect_url;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -342,7 +124,6 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function advanceToScores() {
|
||||
// Show correct answers and answer counts
|
||||
var correctOptions = document.querySelectorAll('[data-correct="True"]');
|
||||
@@ -362,7 +143,7 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
'type': 'advance_to_scores',
|
||||
'host_id': hostId
|
||||
}));
|
||||
}, 0);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Timer
|
||||
@@ -370,18 +151,12 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - questionStartTime;
|
||||
const remaining = Math.max(0, (questionDuration - elapsed) / 1000);
|
||||
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
|
||||
|
||||
|
||||
//document.getElementById('remaining-time').textContent = remaining;
|
||||
|
||||
document.getElementById('time-bar').style.width = ((remaining * 1000) / questionDuration * 100) + '%';
|
||||
if (remaining<3){
|
||||
document.getElementById('time-bar').style.backgroundColor="red";}else if(remaining<5){document.getElementById('time-bar').style.backgroundColor="orange";}
|
||||
document.getElementById('remaining-time').textContent = remaining;
|
||||
|
||||
if (remaining === 0) {
|
||||
advanceToScores();
|
||||
|
||||
} else {
|
||||
requestAnimationFrame(updateTimer);
|
||||
}
|
||||
@@ -464,74 +239,5 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
}));
|
||||
}
|
||||
}
|
||||
let initialOrder = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('sortable-options');
|
||||
if (container) {
|
||||
initialOrder = Array.from(container.children).map(div => div.dataset.index);
|
||||
|
||||
Sortable.create(container, {
|
||||
animation: 150,
|
||||
onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren
|
||||
});
|
||||
|
||||
updateOrderNumbers(); // initial setzen
|
||||
}
|
||||
});
|
||||
|
||||
function updateOrderNumbers() {
|
||||
const container = document.getElementById('sortable-options');
|
||||
const children = container.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const label = children[i].querySelector('.order-number');
|
||||
if (label) {
|
||||
label.textContent = `${i + 1}.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function submitOrderAnswer() {
|
||||
const order_button=document.getElementById("send_order")
|
||||
order_button.disabled=true;
|
||||
order_button.classList.add('opacity-50');
|
||||
const orderedDivs = document.querySelectorAll('#sortable-options button');
|
||||
|
||||
const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value);
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
{% if request.session.mode == "learn" %}
|
||||
timeRemaining = questionDuration;
|
||||
{% endif %}
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: '{{ host_id }}',
|
||||
answer: selectedOrder,
|
||||
time_remaining: timeRemaining
|
||||
}));
|
||||
|
||||
hasAnswered = true;
|
||||
}
|
||||
|
||||
function resetOrderSelection() {
|
||||
const container = document.getElementById('sortable-options');
|
||||
const divs = Array.from(container.children);
|
||||
const indexMap = {};
|
||||
divs.forEach(div => {
|
||||
indexMap[div.dataset.index] = div;
|
||||
});
|
||||
|
||||
// Sort children back to initial order
|
||||
container.innerHTML = '';
|
||||
initialOrder.forEach(i => {
|
||||
container.appendChild(indexMap[i]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,224 +1,41 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% load static %}
|
||||
<script src="{% static 'js/sortablejs/Sortable.min.js' %}"></script>
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-end mb-4">
|
||||
<button onclick="leaveGame()" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
Spiel verlassen
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<p class="text-gray-700 font-bold text-xl mb-4">Frage {{ question_index|add:1 }} von {{ total_questions }}</p>
|
||||
<h1 class="text-3xl font-bold mb-6">{{ question_data.question }}</h1>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="flex gap-1 text-sm"><p class="text-sm">Frage</p>
|
||||
<p class="text-gray-700 font-bold text-sm dark:text-white">{{ question_index|add:1 }}</p>
|
||||
<p class="text-sm">von</p>
|
||||
<p class="text-gray-700 font-bold text-sm dark:text-white">{{ total_questions }}</p>
|
||||
<div class="p-4 bg-blue-50 rounded-lg mb-6">
|
||||
<p class="text-blue-600 text-xl mb-2">Verbleibende Zeit</p>
|
||||
<p id="remaining-time" class="text-6xl font-bold text-blue-700">30</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-black mb-2 border-blue-500 border-4 p-4 text-center rounded-lg md:text-2xl lg:text-4xl">{{ question_data.question}}</h1>
|
||||
|
||||
<div class="w-full bg-gray-300 rounded-full h-4 my-4">
|
||||
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
|
||||
</div>
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
{% if question_data.type != "input" %}
|
||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||
{% for option in question_data.options %}
|
||||
<button data-index="{{ forloop.counter0 }}"
|
||||
data-value="{{ option.value }}"
|
||||
class="
|
||||
{% if forloop.counter0 == 0 %}
|
||||
bg-green-500
|
||||
hover:bg-green-600
|
||||
{% elif forloop.counter0 == 1 %}
|
||||
bg-red-500
|
||||
hover:bg-red-600
|
||||
{% elif forloop.counter0 == 2 %}
|
||||
bg-blue-500
|
||||
hover:bg-blue-600
|
||||
{% elif forloop.counter0 == 3 %}
|
||||
bg-purple-500
|
||||
hover:bg-purple-600
|
||||
{% elif forloop.counter0 == 4 %}
|
||||
bg-orange-500
|
||||
hover:bg-orange-600
|
||||
{% elif forloop.counter0 == 5 %}
|
||||
bg-yellow-500
|
||||
hover:bg-yellow-600
|
||||
{% else %}
|
||||
bg-gray-500
|
||||
hover:bg-gray-600
|
||||
{% endif %}
|
||||
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
|
||||
<button
|
||||
class="p-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option"
|
||||
data-index="{{ forloop.counter0 }}"
|
||||
onclick="submitAnswer({{ forloop.counter0 }});">
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% elif question_data.type == "order" %}
|
||||
|
||||
<div id="sortable-options" class=" mb-4 flex flex-col items-center">
|
||||
{% for option in question_data.options %}
|
||||
|
||||
<button data-index="{{ forloop.counter0 }}"
|
||||
data-value="{{ option.value }}"
|
||||
|
||||
class=" my-2 w-3/4 max-w-xl
|
||||
{% if forloop.counter0 == 0 %}
|
||||
bg-green-500
|
||||
hover:bg-green-600
|
||||
{% elif forloop.counter0 == 1 %}
|
||||
bg-red-500
|
||||
hover:bg-red-600
|
||||
{% elif forloop.counter0 == 2 %}
|
||||
bg-blue-500
|
||||
hover:bg-blue-600
|
||||
{% elif forloop.counter0 == 3 %}
|
||||
bg-purple-500
|
||||
hover:bg-purple-600
|
||||
{% elif forloop.counter0 == 4 %}
|
||||
bg-orange-500
|
||||
hover:bg-orange-600
|
||||
{% elif forloop.counter0 == 5 %}
|
||||
bg-yellow-500
|
||||
hover:bg-yellow-600
|
||||
{% else %}
|
||||
bg-gray-500
|
||||
hover:bg-gray-600
|
||||
{% endif %}
|
||||
p-4 rounded-lg text-white transition-colors duration-200 answer-option overflow-auto break-words hyphens-auto "
|
||||
data-index="{{ forloop.counter0 }}"
|
||||
|
||||
>
|
||||
|
||||
<p class="text-lg flex items-center">
|
||||
<span class="order-number inline-block w-8 h-8 rounded-full bg-white text-gray-800 font-bold text-center leading-8 mr-3">1</span>
|
||||
|
||||
{{ option.value }}
|
||||
</p>
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button id="send_order"
|
||||
onclick="submitOrderAnswer()"
|
||||
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
</button>
|
||||
|
||||
{% elif question_data.type == "guess" %}
|
||||
<form class="flex flex-col items-center gap-4 mt-4">
|
||||
<!-- Slider mit Beschriftung -->
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<input
|
||||
id="textanswer"
|
||||
type="range"
|
||||
min="{{ question_data.min }}"
|
||||
max="{{ question_data.max }}"
|
||||
value="{{ question_data.min }}"
|
||||
step="1"
|
||||
oninput="document.getElementById('range-value').textContent = 'Wert: ' + this.value;"
|
||||
class="w-full answer-option bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]"
|
||||
>
|
||||
<p id="range-value" class="mt-2 font-bold text-gray-800 dark:text-white">Wert: {{ question_data.min }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Abschicken-Button -->
|
||||
<button
|
||||
|
||||
onclick="submitAnswer(-1);"
|
||||
|
||||
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% elif question_data.type == "input" %}
|
||||
<form action="">
|
||||
<input id="textanswer" type="text" placeholder="DEINE ANTWORT"
|
||||
class="answer-option text-center p-2 mb-4 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80 dark:bg-transparent dark:text-white dark:hover:bg-[#2a2f3a]">
|
||||
class="answer-option text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option lg:w-80">
|
||||
<button
|
||||
onclick="submitAnswer( -1 );"
|
||||
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-lg transition-colors duration-200 answer-option "
|
||||
>
|
||||
Abschicken
|
||||
class="mt-2 text-center p-2 bg-gray-100 hover:bg-blue-100 rounded-lg transition-colors duration-200 answer-option">
|
||||
abschicken
|
||||
</button>
|
||||
</form>
|
||||
{% elif question_data.type == "map" %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||
<div id="map" class="h-96"></div>
|
||||
<input type="hidden" value="" id="location" name="location">
|
||||
<div class="mt-4">
|
||||
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||
Abgeben
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
let marker = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
</script>
|
||||
{% else %}
|
||||
<p>aaaaah gibts nicht</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,7 +47,7 @@
|
||||
let hasAnswered = false;
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const participantId = '{{ participant.participant_id }}';
|
||||
const questionStartTime = Date.now();
|
||||
const questionStartTime = {{ start_time }};
|
||||
const questionDuration = '{{ current_question.time_per_question }}'*1000; //Zeit pro Frage (indviduell eingestellt)
|
||||
const gameSocket = initializeGameSocket(joinCode);
|
||||
|
||||
@@ -242,11 +59,6 @@
|
||||
data.action === 'show_scores' &&
|
||||
data.redirect_url) {
|
||||
window.location.href = data.redirect_url;
|
||||
{% if question_data.type == "order" %}
|
||||
submitOrderAnswer();
|
||||
{% elif question_data.type != "map" %}
|
||||
submitAnswer(-1);
|
||||
{% endif %}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -254,22 +66,7 @@
|
||||
wsUtil.handleClose(e);
|
||||
};
|
||||
|
||||
function submitAnswer(optionIndex=0, has_options=true) {
|
||||
if (!has_options){
|
||||
answer = document.getElementById("singleanswer").value;
|
||||
|
||||
if (answer == ""){
|
||||
alert("Bitte eine Antwort eingeben!");
|
||||
return;
|
||||
}
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer:answer,
|
||||
time_remaining: questionDuration
|
||||
}));
|
||||
}
|
||||
function submitAnswer(optionIndex) {
|
||||
if (hasAnswered) return;
|
||||
let input = ""; // Declare input here
|
||||
|
||||
@@ -315,64 +112,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function submitAnswerMap() {
|
||||
answer = document.getElementById("location").value;
|
||||
|
||||
if (answer == ""){
|
||||
alert("Bitte eine Antwort eingeben!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add grey overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||
overlay.style.zIndex = '1000';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.color = 'white';
|
||||
overlay.style.fontSize = '24px';
|
||||
overlay.style.fontWeight = 'bold';
|
||||
overlay.textContent = 'Antwort übermittelt';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const now = Date.now();
|
||||
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer: answer,
|
||||
time_remaining: timeRemaining
|
||||
}));
|
||||
}
|
||||
|
||||
// Timer
|
||||
function updateTimer() {
|
||||
const now = Date.now();
|
||||
const elapsed = now - questionStartTime;
|
||||
const remaining = Math.max(0, (questionDuration - elapsed) / 1000);
|
||||
document.getElementById('time-bar').style.width = ((remaining * 1000) / questionDuration * 100) + '%';
|
||||
if (remaining<3){
|
||||
document.getElementById('time-bar').style.backgroundColor="red";}else if(remaining<5){document.getElementById('time-bar').style.backgroundColor="orange";}
|
||||
const remaining = Math.max(0, Math.ceil((questionDuration - elapsed) / 1000));
|
||||
|
||||
|
||||
//document.getElementById('remaining-time').textContent = remaining;
|
||||
document.getElementById('remaining-time').textContent = remaining;
|
||||
|
||||
if (remaining === 0 && !hasAnswered) {
|
||||
// Auto-submit timeout answer
|
||||
{% if question_data.type == "order" %}
|
||||
submitOrderAnswer();
|
||||
{% else %}
|
||||
submitAnswer(-1);
|
||||
{% endif %}
|
||||
|
||||
|
||||
} else if (remaining > 0) {
|
||||
requestAnimationFrame(updateTimer);
|
||||
}
|
||||
@@ -389,75 +141,5 @@
|
||||
|
||||
// Start timer when page loads
|
||||
updateTimer();
|
||||
|
||||
let initialOrder = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('sortable-options');
|
||||
if (container) {
|
||||
initialOrder = Array.from(container.children).map(div => div.dataset.index);
|
||||
|
||||
Sortable.create(container, {
|
||||
animation: 150,
|
||||
onUpdate: updateOrderNumbers // <--- Nummern aktualisieren nach Sortieren
|
||||
});
|
||||
|
||||
updateOrderNumbers(); // initial setzen
|
||||
}
|
||||
});
|
||||
|
||||
function updateOrderNumbers() {
|
||||
const container = document.getElementById('sortable-options');
|
||||
const children = container.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const label = children[i].querySelector('.order-number');
|
||||
if (label) {
|
||||
label.textContent = `${i + 1}.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function submitOrderAnswer() {
|
||||
if (hasAnswered) return;
|
||||
const order_button=document.getElementById("send_order")
|
||||
order_button.disabled=true;
|
||||
order_button.classList.add('opacity-50');
|
||||
const orderedDivs = document.querySelectorAll('#sortable-options button');
|
||||
|
||||
const selectedOrder = Array.from(orderedDivs).map(div => div.dataset.value);
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
let timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
{% if request.session.mode == "learn" %}
|
||||
timeRemaining = questionDuration;
|
||||
{% endif %}
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer: selectedOrder,
|
||||
time_remaining: timeRemaining
|
||||
}));
|
||||
|
||||
hasAnswered = true;
|
||||
}
|
||||
|
||||
function resetOrderSelection() {
|
||||
const container = document.getElementById('sortable-options');
|
||||
const divs = Array.from(container.children);
|
||||
const indexMap = {};
|
||||
divs.forEach(div => {
|
||||
indexMap[div.dataset.index] = div;
|
||||
});
|
||||
|
||||
// Sort children back to initial order
|
||||
container.innerHTML = '';
|
||||
initialOrder.forEach(i => {
|
||||
container.appendChild(indexMap[i]);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -2,242 +2,35 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white ">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
|
||||
{% if request.session.mode == "learn" or is_host %}
|
||||
<div class="mb-6">
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
|
||||
<p class="text-lg mb-2">{{ question_data.question }}</p>
|
||||
|
||||
{% if question_data.type == "map" %}
|
||||
<!-- Map Question Display -->
|
||||
<div class="mb-4">
|
||||
<div id="map" class="h-96 w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<script>
|
||||
|
||||
const participantAnswers = [
|
||||
{% for participant in participants %}
|
||||
{% if participant.coordinate_answer %}
|
||||
"{{ participant.coordinate_answer }}",
|
||||
{% endif %}
|
||||
{% endfor %}];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Parse the target location from the string format "lat,lng"
|
||||
const targetCoords = '{{ question_data.target_location|escapejs }}'.split(',');
|
||||
let targetLat, targetLng;
|
||||
|
||||
if (targetCoords.length === 2) {
|
||||
targetLat = parseFloat(targetCoords[0].trim());
|
||||
targetLng = parseFloat(targetCoords[1].trim());
|
||||
|
||||
// Normalize longitude to be within -180 to 180
|
||||
targetLng = ((targetLng % 360) + 540) % 360 - 180;
|
||||
|
||||
// Clamp latitude to valid range (-90 to 90)
|
||||
targetLat = Math.max(-90, Math.min(90, targetLat));
|
||||
|
||||
console.log('Normalized coordinates:', { lat: targetLat, lng: targetLng });
|
||||
}
|
||||
|
||||
// Validate coordinates
|
||||
if (isNaN(targetLat) || isNaN(targetLng)) {
|
||||
console.warn('Invalid coordinates, hiding map');
|
||||
document.getElementById('map-container').style.display = 'none';
|
||||
return; // Exit the function early
|
||||
}
|
||||
|
||||
const targetLocation = [targetLat, targetLng];
|
||||
console.log('Using coordinates:', targetLocation);
|
||||
|
||||
// Initialize map centered on target location
|
||||
const map = L.map('map').setView(targetLocation, 7);
|
||||
|
||||
// Check for dark mode
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Choose appropriate map style
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO',
|
||||
noWrap: true
|
||||
}).addTo(map);
|
||||
|
||||
participantAnswers.forEach(coordStr => {
|
||||
const coords = coordStr.split(',').map(Number);
|
||||
if (coords.length === 2 && !coords.some(isNaN)) {
|
||||
const marker = L.marker(coords).addTo(map).bindPopup('Antwort eines Teilnehmers');
|
||||
|
||||
// Linie vom Ziel zum Teilnehmermarker
|
||||
L.polyline([targetLocation, coords], {
|
||||
color: '#f59e0b',
|
||||
weight: 2, // Linienstärke
|
||||
dashArray: '5,5', // gestrichelte Linie
|
||||
opacity: 0.6
|
||||
}).addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Add target marker
|
||||
L.marker(targetLocation, {
|
||||
icon: L.divIcon({
|
||||
html: '🎯',
|
||||
className: 'text-3xl',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32]
|
||||
})
|
||||
}).addTo(map)
|
||||
.bindPopup('Zielort')
|
||||
.openPopup();
|
||||
|
||||
// Add tolerance radius circle if available
|
||||
{% if question_data.tolerance_radius %}
|
||||
(function() {
|
||||
try {
|
||||
const toleranceRadius = parseFloat('{{ question_data.tolerance_radius|escapejs }}'.replace(',', '.'));
|
||||
if (!isNaN(toleranceRadius) && toleranceRadius > 0) {
|
||||
L.circle(targetLocation, {
|
||||
color: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||
fillColor: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||
fillOpacity: 0.2,
|
||||
radius: toleranceRadius
|
||||
}).addTo(map);
|
||||
console.log('Added tolerance radius:', toleranceRadius);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error adding tolerance radius:', e);
|
||||
}
|
||||
})();
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% elif question_data.type != "order" and question_data.type != "guess" %}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% for option in question_data.options %}
|
||||
|
||||
<div id="option-box-{{ forloop.counter0 }}" class=" dark:bg-transparent relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
|
||||
<div id="option-box-{{ forloop.counter0 }}" class="bg-gray-100 relative p-4 rounded-lg overflow-hidden {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
|
||||
|
||||
<div id="option-fill-{{ forloop.counter0 }}" class="absolute left-0 top-0 h-full {% if option.is_correct %}bg-green-300 {% else %}bg-red-300 {% endif %} opacity-50 z-0 transition-all duration-1500 " style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-lg">{{ option.value }}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
|
||||
<p class="text-gray-600" id="option-count-{{ forloop.counter0 }}">0 Antworten</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif question_data.type == "guess" %}
|
||||
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
|
||||
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-lg">Richtige Anwort: {% for option in question_data.options%} {{ option.value }}{% endfor %}</p>
|
||||
<p class="text-sm">(prozentuale Punktevergabe ab Genauigkeit von {{ question_data.tolerance }}%)</p>
|
||||
|
||||
<p class="text-gray-600" id="option-count-0">0 Antworten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="option-box-1" class="relative p-4 rounded-lg border-2 border-red-500 overflow-auto mt-2">
|
||||
<div id="option-fill-1" class="absolute left-0 top-0 h-full bg-red-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-lg">Schätzung zu ungenau</p>
|
||||
<p class="text-gray-600" id="option-count-1">0 Antworten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
|
||||
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-lg">Richtige Reihenfolge</p>
|
||||
<ul class="">
|
||||
{% for option in question_data.options|dictsort:"order" %}
|
||||
<li class="text-lg"> {{ forloop.counter }}. <span class="">{{ option.value }}</span> </li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="text-gray-600" id="option-count-0">0 Antworten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="option-box-1" class="relative p-4 rounded-lg border-2 border-red-500 overflow-auto mt-2">
|
||||
<div id="option-fill-1" class="absolute left-0 top-0 h-full bg-red-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-lg">Falsche Reihenfolge</p>
|
||||
<p class="text-gray-600" id="option-count-1">0 Antworten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
||||
|
||||
<div class="flex flex-col items-center justify-center text-center pt-4">
|
||||
{% if participant.last_answer_correct %}
|
||||
<!-- Richtig-Symbol -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor"
|
||||
class="text-green-500 size-40">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
<!-- Feedback-Text -->
|
||||
<div id="antwort-richtig"
|
||||
class="pt-3 text-gray-600 dark:text-white font-bold max-w-xs md:max-w-sm">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
|
||||
{% elif participant.last_answer_correct == False %}
|
||||
<!-- Falsch-Symbol -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor"
|
||||
class="text-red-500 size-40">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
<!-- Feedback-Text -->
|
||||
<div id="antwort-falsch"
|
||||
class="pt-3 text-gray-600 dark:text-white font-bold max-w-xs md:max-w-sm">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-blue-600 font-bold text-md">+{{ participant.last_score }} Punkte</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
{% if request.session.mode == "learn" %}
|
||||
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
|
||||
{% endif %}
|
||||
|
||||
<div id="scoreboard" class="leading-relaxed">
|
||||
{% for participant in participants %}
|
||||
{% if request.session.mode == "learn" %}
|
||||
{% if participant == my_participant or is_host %}
|
||||
<div class="dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border {% if forloop.counter <= 3 %}border-yellow-500{% else %}border-transparent{% endif %}">
|
||||
<div class="mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border {% if forloop.counter <= 3 %}border-yellow-500{% else %}border-transparent{% endif %}">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<div class="text-lg font-bold flex items-center gap-2">
|
||||
{% if forloop.counter == 1 %}
|
||||
@@ -249,115 +42,16 @@
|
||||
{% else %}
|
||||
{{ forloop.counter }}.
|
||||
{% endif %}
|
||||
<span class="display_name text-gray-600 font-black text-2xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
|
||||
<span class="display_name text-gray-600 font-black text-2xl " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
|
||||
</div class="flex flex-col items-start">
|
||||
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="text-gray-600 font-bold show-score" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
|
||||
{% if participant == my_participant or request.session.mode == "learn"%}
|
||||
<p class="text-blue-600 font-bold text-sm">+{{ participant.last_score }} Punkte</p>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% if participant == my_participant or request.session.mode == "learn" %}
|
||||
{% if participant.last_answer_correct %}
|
||||
|
||||
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-green-500 font-black size-10">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
<div class="dark:text-white pt-3 text-center text-gray-600 font-bold" id="antwort-richtig">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% elif participant.last_answer_correct == False %}
|
||||
|
||||
|
||||
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-red-500 font-black size-10">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
<div class="dark:text-white pt-3 text-center text-gray-600 font-bold" id="antwort-falsch">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
<div class="flex justify-center border-t-2 border-gray-500 gap-1">
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_host %}
|
||||
{% if is_last_question %}
|
||||
{% if is_last_question and request.session.mode != "learn" %}
|
||||
<button id="winner-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Siegerpodest
|
||||
</button>
|
||||
|
||||
{% else %}
|
||||
<button id="finish-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Quiz beenden
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if request.session.mode == "learn" %}
|
||||
<button id="next-button" class="mt-4 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>
|
||||
{% else %}
|
||||
<button id="scoreboard-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Punkteübersicht
|
||||
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if is_last_question %}
|
||||
|
||||
{% endif %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
{% if is_host %}
|
||||
{% include 'play/game/sound.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function triggerConfetti() {
|
||||
const end = Date.now() + 30 * 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);
|
||||
})();
|
||||
}
|
||||
async function start() {
|
||||
|
||||
elements=document.getElementsByClassName("show-score");
|
||||
@@ -393,17 +87,146 @@
|
||||
|
||||
|
||||
{% if is_last_question %}
|
||||
start();
|
||||
bounce();
|
||||
{% if request.session.mode == "learn" %}
|
||||
triggerConfetti();
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% else %}
|
||||
start();
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% if participant == my_participant or request.session.mode == "learn" %}
|
||||
{% if participant.last_answer_correct %}
|
||||
|
||||
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-green-500 font-black size-10">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
<div class=" pt-3 text-center text-gray-600 font-bold" id="antwort-richtig">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% elif participant.last_answer_correct == False %}
|
||||
|
||||
|
||||
<div class="mt-1 flex justify-center border-t-2 border-gray-300 gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="pt-2 text-center text-red-500 font-black size-10">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
|
||||
|
||||
<div class=" pt-3 text-center text-gray-600 font-bold" id="antwort-falsch">
|
||||
Hier erscheint gleich dein Feedback...
|
||||
</div>
|
||||
<div class="flex justify-center border-t-2 border-gray-500 gap-1">
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_host %}
|
||||
{% if is_last_question %}
|
||||
<button id="finish-button" class="mt-4 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="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Nächste Frage
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% 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 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' %}
|
||||
{% if is_host %}
|
||||
{% include 'play/game/sound.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
const richtigeAntworten = [
|
||||
"Deine Antwort ist nicht falsch. Sie ist richtig!",
|
||||
"Ja, du kannst es!",
|
||||
@@ -478,10 +301,9 @@ if (antwortRichtig) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'game_state_update' &&
|
||||
data.redirect_url &&
|
||||
(data.action === 'next_question' || data.action === 'finish_game' || data.action === 'show_winner_podest'|| data.action === 'show_scoreboard')) {
|
||||
(data.action === 'next_question' || data.action === 'finish_game')) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
else if (data.type === 'answer_stats') {
|
||||
} else if (data.type === 'answer_stats') {
|
||||
updateAnswerStats(data.stats);
|
||||
}
|
||||
};
|
||||
@@ -507,30 +329,7 @@ if (antwortRichtig) {
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
var podestButton = document.getElementById('winner-button');
|
||||
if (podestButton) {
|
||||
podestButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'winner_podest',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{% else %}
|
||||
|
||||
var scoreboardButton = document.getElementById('scoreboard-button');
|
||||
if (scoreboardButton) {
|
||||
scoreboardButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'scoreboard',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var nextButton = document.getElementById('next-button');
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener('click', function() {
|
||||
@@ -538,7 +337,7 @@ if (antwortRichtig) {
|
||||
'type': 'next_question',
|
||||
'host_id': hostId
|
||||
}));
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
|
||||
<h1 class="text-3xl font-bold mb-6">Ergebnisse</h1>
|
||||
|
||||
|
||||
|
||||
<div class="mb-6">
|
||||
{% if request.session.mode != "learn" %}
|
||||
<h2 class="text-xl font-bold mb-4">Punktestand</h2>
|
||||
{% endif %}
|
||||
|
||||
<div id="scoreboard" class="leading-relaxed">
|
||||
{% for participant in participants %}
|
||||
{% if request.session.mode != "learn" %}
|
||||
{% if participant == my_participant or is_host %}
|
||||
<div class="dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border
|
||||
{% if forloop.counter <= 3 %}
|
||||
{% if forloop.counter == 1 %}border-yellow-400{% endif %}
|
||||
{% if forloop.counter == 2 %}border-gray-400{% endif %}
|
||||
{% if forloop.counter == 3 %}border-orange-400{% endif %}
|
||||
{% else %}
|
||||
border-transparent{% endif %}">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<div class="text-4xl font-bold flex items-center gap-2">
|
||||
{% if forloop.counter == 1 %}
|
||||
🥇
|
||||
{% elif forloop.counter == 2 %}
|
||||
🥈
|
||||
{% elif forloop.counter == 3 %}
|
||||
🥉
|
||||
{% else %}
|
||||
{{ forloop.counter }}.
|
||||
{% endif %}
|
||||
<span class="display_name text-gray-600 font-black text-3xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
|
||||
</div>
|
||||
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score text-xl" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
|
||||
{% if participant == my_participant or request.session.mode == "learn"%}
|
||||
<p class="text-blue-600 font-bold text-md">+{{ participant.last_score }} Punkte</p>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_host %}
|
||||
{% if is_last_question %}
|
||||
{% if is_last_question and request.session.mode != "learn" %}
|
||||
<button id="winner-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Siegerpodest
|
||||
</button>
|
||||
|
||||
{% else %}
|
||||
<button id="finish-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Quiz beenden
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
||||
<button id="next-button" class="mt-4 w-full p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Nächste Frage
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if is_last_question %}
|
||||
|
||||
{% endif %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
{% if is_host %}
|
||||
{% include 'play/game/sound.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
async function start() {
|
||||
|
||||
elements=document.getElementsByClassName("show-score");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i];
|
||||
|
||||
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
|
||||
let goal=Number(el.getAttribute('data-score'));
|
||||
let interval= setInterval(function(){
|
||||
el.innerHTML = counter + " Punkte";
|
||||
|
||||
|
||||
if(counter<=goal){
|
||||
el.innerHTML = counter + " Punkte";
|
||||
counter+=8;
|
||||
|
||||
}
|
||||
else{
|
||||
|
||||
el.innerHTML = goal + " Punkte";
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
}, 20);}}
|
||||
|
||||
function bounce(){
|
||||
|
||||
const names = document.getElementsByClassName("display_name");
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
names[i].classList.add("animate-bounce");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{% if is_last_question %}
|
||||
start();
|
||||
bounce();
|
||||
|
||||
|
||||
{% else %}
|
||||
start();
|
||||
{% endif %}
|
||||
|
||||
const richtigeAntworten = [
|
||||
"Deine Antwort ist nicht falsch. Sie ist richtig!",
|
||||
"Ja, du kannst es!",
|
||||
"So kann es weiter gehen.",
|
||||
"Ja, deine Antwort ist korrekt. Was soll ich dazu noch sagen? SUPER!",
|
||||
"SUPER GUT.",
|
||||
"Volltreffer!",
|
||||
"Bravo! Das sitzt!",
|
||||
"Na bitte, geht doch!",
|
||||
"Schön. Einfach schön.",
|
||||
"Exzellent kombiniert, Sherlock!",
|
||||
"Punkt für dich!",
|
||||
"Das war meisterhaft.",
|
||||
"Korrekt - du scheinst das zu können!",
|
||||
"So sieht Erfolg aus!",
|
||||
"Glänzend gelöst!",
|
||||
"Das war keine Glückssache - das war Können!",
|
||||
"Treffer, versenkt!",
|
||||
"Du hast es drauf!",
|
||||
"Da merkt man: Profi am Werk.",
|
||||
"Richtige Antwort - dafür gibt es viele Punkte!",
|
||||
"Weiter so!",
|
||||
"Wow - das war richtig gut!"
|
||||
];
|
||||
|
||||
const falscheAntworten = [
|
||||
"Oops! War wohl nicht dein Tag.",
|
||||
"Knapp daneben ist auch vorbei!",
|
||||
"Das war... kreativ!",
|
||||
"Wenn man schon alles wüsste, wäre das Quiz doch sinnlos.",
|
||||
"Deine Antwort war einzigartig - leider falsch!",
|
||||
"Na gut, wenigstens hast du es probiert.",
|
||||
"Das war eine interessante Theorie...",
|
||||
"Ein Genie irrt sich auch mal.",
|
||||
"Eine gute Antwort - aber nicht auf diese Frage.",
|
||||
"Das ist leider falsch. Mehr gibt es nicht zu sagen.",
|
||||
"Gute Idee! Aber leider nicht richtig.",
|
||||
"Oh weh! Ein Fehler. Naja, einer ist keiner.",
|
||||
"Die Antwort klang richtig, sie ist es aber nicht.",
|
||||
"Interessanter Ansatz. Nicht hilfreich, aber interessant.",
|
||||
"Nenn das lieber „alternative Fakten“.",
|
||||
"Du hast die richtige Antwort doch gewusst: 'Die Seite hat nur nicht geladen.'",
|
||||
"Jeder fängt mal klein an!",
|
||||
"Kopf hoch - weiter geht's!",
|
||||
"Nobody is perfect!",
|
||||
"Du schaffst das - vielleicht ab jetzt.",
|
||||
"Falsche Antwort, aber guter Wille!",
|
||||
"Übung macht den Quiz-Meister.",
|
||||
"Beim nächsten Mal triffst du!",
|
||||
"Das war nur zum Aufwärmen, oder?",
|
||||
"Kein Problem - weiter geht's.",
|
||||
"Gute Frage - aber falsche Antwort."
|
||||
];
|
||||
|
||||
|
||||
const antwortRichtig = document.getElementById("antwort-richtig");
|
||||
const antwortFalsch = document.getElementById("antwort-falsch");
|
||||
|
||||
if (antwortRichtig) {
|
||||
antwortRichtig.textContent = richtigeAntworten[Math.floor(Math.random() * richtigeAntworten.length)];
|
||||
} else if (antwortFalsch) {
|
||||
antwortFalsch.textContent = falscheAntworten[Math.floor(Math.random() * falscheAntworten.length)];
|
||||
}
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const hostId = '{{ host_id }}';
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const gameSocket = new WebSocket(
|
||||
`${wsScheme}://${window.location.host}/ws/game/${joinCode}/`
|
||||
);
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'game_state_update' &&
|
||||
data.redirect_url &&
|
||||
(data.action === 'next_question' || data.action === 'finish_game' || data.action === 'show_winner_podest'|| data.action === 'show_scoreboard')) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
else if (data.type === 'answer_stats') {
|
||||
updateAnswerStats(data.stats);
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
// Normal closure (code 1000) oder Navigation zu einer anderen Seite (code 1001)
|
||||
if (e.code === 1000 || e.code === 1001) {
|
||||
console.log('Game socket closed normally');
|
||||
} else {
|
||||
console.error('Game socket closed unexpectedly', e.code);
|
||||
}
|
||||
};
|
||||
|
||||
{% if is_host %}
|
||||
// Add click handlers for host buttons
|
||||
{% if is_last_question %}
|
||||
var finishButton = document.getElementById('finish-button');
|
||||
if (finishButton) {
|
||||
finishButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'finish_game',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
var podestButton = document.getElementById('winner-button');
|
||||
if (podestButton) {
|
||||
podestButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'winner_podest',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{% else %}
|
||||
|
||||
var scoreboardButton = document.getElementById('scoreboard-button');
|
||||
if (scoreboardButton) {
|
||||
scoreboardButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'scoreboard',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var nextButton = document.getElementById('next-button');
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'next_question',
|
||||
'host_id': hostId
|
||||
}));
|
||||
}, { once: true });
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
function updateAnswerStats(stats) {
|
||||
const total = Object.values(stats).reduce((a, b) => a + b, 0) || 1;
|
||||
|
||||
for (var optionIndex in stats) {
|
||||
if (stats.hasOwnProperty(optionIndex)) {
|
||||
var countElement = document.getElementById('option-count-' + optionIndex);
|
||||
var fillElement = document.getElementById('option-fill-' + optionIndex);
|
||||
|
||||
if (countElement) {
|
||||
countElement.textContent = stats[optionIndex] + ' Antwort(en)';
|
||||
}
|
||||
|
||||
if (fillElement) {
|
||||
const percentage = Math.round((stats[optionIndex] / total) * 100);
|
||||
fillElement.style.width = percentage + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request answer stats when page loads
|
||||
gameSocket.onopen = function(e) {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'get_answer_stats'
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,594 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if is_last_question and is_host %}
|
||||
<!-- Siegerpodest -->
|
||||
<div class="min-h-screen">
|
||||
<h2 class="w-full flex justify-center dark:text-white font-bold text-2xl ">🏆 Siegerpodest</h2>
|
||||
<h4 class="w-full flex justify-center dark:text-white font-bold">Wer ist hier der Quiz-Master? </h4>
|
||||
|
||||
{% with participant=participants.0 %}
|
||||
<div class="w-full flex justify-center">
|
||||
<h1 id="winner-text" class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold hidden animate-ping px-2 max-w-screen-sm text-center break-words">
|
||||
Glückwunsch, {{ participant.display_name }}!
|
||||
</h1>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="podest-container" class="w-full absolute bottom-0 flex justify-center items-end gap-2 md:gap-4 mb-2 dark:text-white">
|
||||
|
||||
|
||||
<!-- Platz 2 links -->
|
||||
{% with participant=participants.1 %}
|
||||
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
|
||||
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-2xl lg:text-3xl opacity-0 transition-opacity duration-1000 dark:text-white hyphens-auto" id="display_name-1">{{ participant.display_name }}</span>
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white font-bold show-score text-sm md:text-lg lg:text-xl text-gray-400" id="score-1">{{ participant.score }}</p>
|
||||
<div data-final-height="33" id="podium-1" class="border-2 border-gray-200 transition-all duration-1000 ease-out overflow-hidden podium h-24 md:h-[12rem] lg:h-[16rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-gray-400 via-gray-300 to-gray-300 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
|
||||
<span class="size-10 md:size-18 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-3xl md:text-5xl lg:text-6xl">🥈</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
<!-- Platz 1 mitte -->
|
||||
{% with participant=participants.0 %}
|
||||
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
|
||||
<div id="resultFirst"></div>
|
||||
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-3xl lg:text-4xl opacity-0 transition-opacity duration-1000 bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent hyphens-auto" id="display_name-0">{{ participant.display_name }}</span>
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white text-yellow-500 font-bold show-score text-sm md:text-lg lg:text-xl" id="score-0">{{ participant.score }}</p>
|
||||
<div data-final-height="45" id="podium-0" class="border-2 border-yellow-500 transition-all duration-1000 ease-out overflow-hidden podium h-32 md:h-[16rem] lg:h-[22rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-yellow-400 via-yellow-300 to-yellow-600 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
|
||||
<span id="firstMedal" class="size-12 md:size-22 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-4xl md:text-6xl lg:text-7xl">🥇</span><br>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
<!-- Platz 3 rechts -->
|
||||
{% with participant=participants.2 %}
|
||||
<div class="flex flex-col shrink items-center break-words max-w-[6rem] md:max-w-[8rem] lg:max-w-[10rem] overflow-x-auto pt-10">
|
||||
<span class="display_name p-2 text-gray-600 font-black text-xl md:text-2xl lg:text-3xl dark:text-white opacity-0 transition-opacity duration-1000 hyphens-auto" id="display_name-2">{{ participant.display_name }}</span>
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="opacity-0 transition-opacity duration-1000 dark:text-white text-orange-500 font-bold show-score text-sm md:text-lg lg:text-xl" id="score-2">{{ participant.score }}</p>
|
||||
<div data-final-height="25" id="podium-2" class="border-2 border-orange-500 transition-all duration-1000 ease-out overflow-hidden podium h-20 md:h-[10rem] lg:h-[14rem] w-24 md:w-32 lg:w-40 rounded-t-md font-bold text-center text-base shadow-lg bg-gradient-to-b from-orange-500 via-orange-400 to-orange-700 flex flex-col items-center justify-center text-black dark:text-white cursor-pointer leading-tight">
|
||||
<span class="size-10 md:size-18 rounded-full bg-white text-black text-center flex items-center justify-center font-bold text-3xl md:text-5xl lg:text-6xl">🥉</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="thanks" class="hidden -mt-10">
|
||||
<div class="w-full flex justify-center">
|
||||
<h1 class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold px-2 max-w-screen-sm text-center break-words">
|
||||
Vielen Dank fürs Spielen!
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<button id="finish-button" class=" w-3/4 md:w-1/2 bottom-0 left-0 p-3 rounded-full bg-blue-600 text-white font-bold hover:bg-blue-700 transition-colors duration-200">
|
||||
Quiz beenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
|
||||
|
||||
{% with participant=participants.0 %}
|
||||
<div class="w-full flex justify-center">
|
||||
<h1 id="winner-text" class="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-600 bg-clip-text text-transparent font-bold hidden animate-ping px-2 max-w-screen-sm text-center break-words">
|
||||
Glückwunsch, {{ participant.display_name }}!
|
||||
</h1>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 dark:bg-transparent dark:text-white">
|
||||
<h1 id="result-text" class="text-3xl w-full flex justify-center dark:text-white font-bold">🏆 Wer ist hier der Quiz-Master?</h1>
|
||||
|
||||
<h4 id="info" class="mt-4 w-full flex justify-center dark:text-white font-bold">Nach der Siegerehrung siehst du deine Platzierung!</h4>
|
||||
|
||||
|
||||
|
||||
<div id="participant-score" class="mb-6 ">
|
||||
|
||||
|
||||
<div id="scoreboard" class="leading-relaxed">
|
||||
{% for participant in participants %}
|
||||
{% if request.session.mode != "learn" %}
|
||||
{% if participant == my_participant or is_host %}
|
||||
<div id="player_score" class="hidden dark:bg-transparent mt-2 shadow-sm p-4 bg-gray-100 rounded-lg border-2 border {% if forloop.counter <= 3 %}
|
||||
{% if forloop.counter == 1 %}border-yellow-400{% endif %}
|
||||
{% if forloop.counter == 2 %}border-gray-400{% endif %}
|
||||
{% if forloop.counter == 3 %}border-orange-400{% endif %}
|
||||
{% else %}
|
||||
border-transparent{% endif %}">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<div class="text-4xl font-bold flex items-center gap-2">
|
||||
{% if forloop.counter == 1 %}
|
||||
🥇
|
||||
{% elif forloop.counter == 2 %}
|
||||
🥈
|
||||
{% elif forloop.counter == 3 %}
|
||||
🥉
|
||||
{% else %}
|
||||
{{ forloop.counter }}.
|
||||
{% endif %}
|
||||
<span class="display_name text-gray-600 font-black text-3xl dark:text-white " id="display_name-{{ forloop.counter0 }}">{{ participant.display_name }}</span>
|
||||
</div class="flex flex-col items-start">
|
||||
|
||||
<p data-score="{{ participant.score }}" data-last-score="{{ participant.last_score }}" class="dark:text-white text-gray-600 font-bold show-score text-xl" id="score-{{ forloop.counter0 }}">{{ participant.score }} Punkte</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.podium {
|
||||
height: 0;
|
||||
|
||||
}
|
||||
.spotlight-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -800px; /* Start oben außerhalb */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 15rem;
|
||||
height: 700px;
|
||||
pointer-events: none;
|
||||
|
||||
background: radial-gradient(
|
||||
ellipse 60% 100% at center bottom,
|
||||
rgba(254, 254, 178, 0.9) 0%,
|
||||
rgba(252, 236, 132, 0.7) 25%,
|
||||
rgba(255, 230, 130, 0.5) 55%,
|
||||
rgba(255, 210, 100, 0.1) 85%,
|
||||
rgba(255, 180, 70, 0) 100%
|
||||
);
|
||||
|
||||
filter: blur(24px) brightness(1.2);
|
||||
opacity: 0; /* Start unsichtbar */
|
||||
animation: slideInOutSpotlight 6s ease forwards , fadeOutSpotlight 6s 6s ease forwards; /* Dauer anpassen */
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0 0 60px 30px rgba(255, 240, 150, 0.15);
|
||||
}
|
||||
|
||||
@keyframes slideInOutSpotlight {
|
||||
0% {
|
||||
top: -800px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: -640px; /* Endposition */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: -640px;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeOutSpotlight {
|
||||
0% {
|
||||
top: -640px;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
100% {
|
||||
top: -640px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block extra_js %}
|
||||
{% if is_last_question %}
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const animationDuration = 1500; // Dauer der Wachstumsanimation
|
||||
const waitTime=3000;
|
||||
const podiums = [
|
||||
|
||||
{ id: 'podium-2', delay: 2000 }, // immer vorheriges von Bedeutung
|
||||
{ id: 'podium-1', delay: 2000 },
|
||||
{ id: 'podium-0', delay: 0 },
|
||||
];
|
||||
|
||||
|
||||
let totalDelay = 0;
|
||||
|
||||
podiums.forEach(({ delay }) => {
|
||||
totalDelay += delay + waitTime;
|
||||
});
|
||||
const totalAnimationTime = totalDelay + animationDuration;
|
||||
setTimeout(() => {
|
||||
show_score();
|
||||
}, totalAnimationTime);
|
||||
|
||||
totalDelay = 0;
|
||||
|
||||
|
||||
|
||||
podiums.forEach(({ id, delay }) => {
|
||||
|
||||
const podium = document.getElementById(id);
|
||||
if (!podium) return;
|
||||
|
||||
const finalHeight = parseInt(podium.dataset.finalHeight) || 160;
|
||||
podium.style.transition = `height ${animationDuration}ms ease-in-out`;
|
||||
podium.style.height = '0px';
|
||||
|
||||
// Wachstum starten nach gesamter Wartezeit
|
||||
setTimeout(() => {
|
||||
podium.style.height = `${finalHeight}vh`;
|
||||
}, totalDelay);
|
||||
|
||||
// Name & Score anzeigen nach Balken-Animation
|
||||
setTimeout(() => {
|
||||
|
||||
if (id === 'podium-0') {
|
||||
let name = document.getElementById(`display_name-0`);
|
||||
let score = document.getElementById(`score-0`);
|
||||
let winnerText= document.getElementById(`winner-text`);
|
||||
|
||||
let thanks= document.getElementById(`thanks`);
|
||||
// 1. Lichtkegel anzeigen
|
||||
if (resultFirst) {
|
||||
resultFirst.classList.add('spotlight-wrapper');
|
||||
score.classList.add('opacity-0');
|
||||
name.classList.add('opacity-0');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 2. Nach kurzer Wartezeit Name & Score anzeigen
|
||||
setTimeout(() => {
|
||||
|
||||
if (name) {
|
||||
name.classList.add('opacity-100', 'animate-bounce','spotlight-text'); // z. B. sichtbar + Effekt
|
||||
}
|
||||
|
||||
if (winnerText) {
|
||||
winnerText.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (thanks) {
|
||||
thanks.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (score) {
|
||||
score.classList.add('opacity-100'); // z. B. sichtbar + Effekt
|
||||
}
|
||||
triggerConfetti();
|
||||
|
||||
}, 2000);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.body.style.overflow = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
const index = id.split('-')[1];
|
||||
const name = document.getElementById(`display_name-${index}`);
|
||||
const score = document.getElementById(`score-${index}`);
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
if (name && id !== 'podium-0') setTimeout(() => {
|
||||
name.classList.add('opacity-100');
|
||||
name.classList.add('animate-bounce');
|
||||
|
||||
if (score) score.classList.add('opacity-100');
|
||||
}, waitTime);
|
||||
|
||||
}, totalDelay + animationDuration);
|
||||
|
||||
|
||||
totalDelay += delay + waitTime; // wichtig: aufsummieren
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Hilfsfunktion für Pausen
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
function triggerConfetti() {
|
||||
const end = Date.now() + 30 * 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);
|
||||
})();
|
||||
}
|
||||
|
||||
function show_score() {
|
||||
|
||||
let player_score=document.getElementById('player_score');
|
||||
player_score.classList.remove('hidden');
|
||||
info=document.getElementById('info');
|
||||
info.classList.add('hidden');
|
||||
resultText=document.getElementById('result-text');
|
||||
resultText.textContent="Dein Ergebnis";
|
||||
|
||||
|
||||
|
||||
const myName = "{{ my_participant.id|escapejs }}";
|
||||
const firstPlaceName = "{{ participants.0.id|escapejs }}";
|
||||
|
||||
if (myName === firstPlaceName) {
|
||||
let winnerText= document.getElementById(`winner-text`);
|
||||
winnerText.classList.remove('hidden');
|
||||
triggerConfetti();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
{% include 'play/game/common_scripts.html' %}
|
||||
{% if is_host %}
|
||||
{% include 'play/game/sound.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
async function start() {
|
||||
|
||||
elements=document.getElementsByClassName("show-score");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements[i];
|
||||
|
||||
let counter=Number(el.getAttribute('data-score'))-Number(el.getAttribute('data-last-score'));
|
||||
let goal=Number(el.getAttribute('data-score'));
|
||||
let interval= setInterval(function(){
|
||||
el.innerHTML = counter + " Punkte";
|
||||
|
||||
|
||||
if(counter<=goal){
|
||||
el.innerHTML = counter + " Punkte";
|
||||
counter+=8;
|
||||
|
||||
}
|
||||
else{
|
||||
|
||||
el.innerHTML = goal + " Punkte";
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
}, 20);}}
|
||||
|
||||
function bounce(){
|
||||
|
||||
const names = document.getElementsByClassName("display_name");
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
names[i].classList.add("animate-bounce");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{% if is_last_question %}
|
||||
start();
|
||||
bounce();
|
||||
|
||||
|
||||
{% else %}
|
||||
start();
|
||||
{% endif %}
|
||||
|
||||
const richtigeAntworten = [
|
||||
"Deine Antwort ist nicht falsch. Sie ist richtig!",
|
||||
"Ja, du kannst es!",
|
||||
"So kann es weiter gehen.",
|
||||
"Ja, deine Antwort ist korrekt. Was soll ich dazu noch sagen? SUPER!",
|
||||
"SUPER GUT.",
|
||||
"Volltreffer!",
|
||||
"Bravo! Das sitzt!",
|
||||
"Na bitte, geht doch!",
|
||||
"Schön. Einfach schön.",
|
||||
"Exzellent kombiniert, Sherlock!",
|
||||
"Punkt für dich!",
|
||||
"Das war meisterhaft.",
|
||||
"Korrekt - du scheinst das zu können!",
|
||||
"So sieht Erfolg aus!",
|
||||
"Glänzend gelöst!",
|
||||
"Das war keine Glückssache - das war Können!",
|
||||
"Treffer, versenkt!",
|
||||
"Du hast es drauf!",
|
||||
"Da merkt man: Profi am Werk.",
|
||||
"Richtige Antwort - dafür gibt es viele Punkte!",
|
||||
"Weiter so!",
|
||||
"Wow - das war richtig gut!"
|
||||
];
|
||||
|
||||
const falscheAntworten = [
|
||||
"Oops! War wohl nicht dein Tag.",
|
||||
"Knapp daneben ist auch vorbei!",
|
||||
"Das war... kreativ!",
|
||||
"Wenn man schon alles wüsste, wäre das Quiz doch sinnlos.",
|
||||
"Deine Antwort war einzigartig - leider falsch!",
|
||||
"Na gut, wenigstens hast du es probiert.",
|
||||
"Das war eine interessante Theorie...",
|
||||
"Ein Genie irrt sich auch mal.",
|
||||
"Eine gute Antwort - aber nicht auf diese Frage.",
|
||||
"Das ist leider falsch. Mehr gibt es nicht zu sagen.",
|
||||
"Gute Idee! Aber leider nicht richtig.",
|
||||
"Oh weh! Ein Fehler. Naja, einer ist keiner.",
|
||||
"Die Antwort klang richtig, sie ist es aber nicht.",
|
||||
"Interessanter Ansatz. Nicht hilfreich, aber interessant.",
|
||||
"Nenn das lieber „alternative Fakten“.",
|
||||
"Du hast die richtige Antwort doch gewusst: 'Die Seite hat nur nicht geladen.'",
|
||||
"Jeder fängt mal klein an!",
|
||||
"Kopf hoch - weiter geht's!",
|
||||
"Nobody is perfect!",
|
||||
"Du schaffst das - vielleicht ab jetzt.",
|
||||
"Falsche Antwort, aber guter Wille!",
|
||||
"Übung macht den Quiz-Meister.",
|
||||
"Beim nächsten Mal triffst du!",
|
||||
"Das war nur zum Aufwärmen, oder?",
|
||||
"Kein Problem - weiter geht's.",
|
||||
"Gute Frage - aber falsche Antwort."
|
||||
];
|
||||
|
||||
|
||||
const antwortRichtig = document.getElementById("antwort-richtig");
|
||||
const antwortFalsch = document.getElementById("antwort-falsch");
|
||||
|
||||
if (antwortRichtig) {
|
||||
antwortRichtig.textContent = richtigeAntworten[Math.floor(Math.random() * richtigeAntworten.length)];
|
||||
} else if (antwortFalsch) {
|
||||
antwortFalsch.textContent = falscheAntworten[Math.floor(Math.random() * falscheAntworten.length)];
|
||||
}
|
||||
const joinCode = '{{ quiz_game.join_code }}';
|
||||
const hostId = '{{ host_id }}';
|
||||
const wsScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const gameSocket = new WebSocket(
|
||||
`${wsScheme}://${window.location.host}/ws/game/${joinCode}/`
|
||||
);
|
||||
|
||||
gameSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'game_state_update' &&
|
||||
data.redirect_url &&
|
||||
(data.action === 'next_question' || data.action === 'finish_game' || data.action === 'show_winner_podest')) {
|
||||
window.location.href = data.redirect_url;
|
||||
}
|
||||
else if (data.type === 'answer_stats') {
|
||||
updateAnswerStats(data.stats);
|
||||
}
|
||||
};
|
||||
|
||||
gameSocket.onclose = function(e) {
|
||||
// Normal closure (code 1000) oder Navigation zu einer anderen Seite (code 1001)
|
||||
if (e.code === 1000 || e.code === 1001) {
|
||||
console.log('Game socket closed normally');
|
||||
} else {
|
||||
console.error('Game socket closed unexpectedly', e.code);
|
||||
}
|
||||
};
|
||||
|
||||
{% if is_host %}
|
||||
// Add click handlers for host buttons
|
||||
{% if is_last_question %}
|
||||
var finishButton = document.getElementById('finish-button');
|
||||
if (finishButton) {
|
||||
finishButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'finish_game',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
var podestButton = document.getElementById('winner-button');
|
||||
if (podestButton) {
|
||||
podestButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'winner_podest',
|
||||
'host_id': hostId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
{% else %}
|
||||
|
||||
var nextButton = document.getElementById('next-button');
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener('click', function() {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'next_question',
|
||||
'host_id': hostId
|
||||
}));
|
||||
}, { once: true });
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
function updateAnswerStats(stats) {
|
||||
const total = Object.values(stats).reduce((a, b) => a + b, 0) || 1;
|
||||
|
||||
for (var optionIndex in stats) {
|
||||
if (stats.hasOwnProperty(optionIndex)) {
|
||||
var countElement = document.getElementById('option-count-' + optionIndex);
|
||||
var fillElement = document.getElementById('option-fill-' + optionIndex);
|
||||
|
||||
if (countElement) {
|
||||
countElement.textContent = stats[optionIndex] + ' Antwort(en)';
|
||||
}
|
||||
|
||||
if (fillElement) {
|
||||
const percentage = Math.round((stats[optionIndex] / total) * 100);
|
||||
fillElement.style.width = percentage + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request answer stats when page loads
|
||||
gameSocket.onopen = function(e) {
|
||||
gameSocket.send(JSON.stringify({
|
||||
'type': 'get_answer_stats'
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,10 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6 dark:bg-transparent dark:border-2 dark:text-white">
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center text-blue-600">Spieler einrichten</h1>
|
||||
|
||||
<div class="text-center mb-6">
|
||||
@@ -18,20 +17,20 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="join_code" value="{{join_code}}">
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4 dark:bg-transparent">
|
||||
<p class="text-sm text-gray-600 mb-2 dark:text-white">Wähle einen Anzeigenamen für das Spiel:</p>
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-4">
|
||||
<p class="text-sm text-gray-600 mb-2">Wähle einen Anzeigenamen für das Spiel:</p>
|
||||
<input type="text"
|
||||
name="display_name"
|
||||
required
|
||||
maxlength="20"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Dein Spielername">
|
||||
<p class="text-xs text-gray-500 mt-2 dark:text-white">Maximal 20 Zeichen</p>
|
||||
<p class="text-xs text-gray-500 mt-2">Maximal 20 Zeichen</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center space-x-4">
|
||||
<a href="/"
|
||||
class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200 dark:bg-transparent dark:hover:bg-[#2a2f3a]">
|
||||
class="px-6 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors duration-200">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid place-items-center min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2">
|
||||
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white dark:bg-transparent dark:text-white">
|
||||
<div class="sm:grid sm:place-items-center sm:min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2">
|
||||
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white">
|
||||
<h1 class="text-2xl text-center font-bold mb-4">Spiel beitreten</h1>
|
||||
<form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4">
|
||||
{% csrf_token %}
|
||||
<div class="flex gap-3 items-center justify-center">
|
||||
<input type="number" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider text-black" autofocus>
|
||||
<input type="number" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider" autofocus>
|
||||
<button type="submit" class="h-12 px-6 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 active:scale-95 group" disabled>
|
||||
<span class="hidden sm:block">Beitreten</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform group-hover:translate-x-1">
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6 dark:bg-transparent dark:text-white">
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<!-- Quiz Info -->
|
||||
<div class="text-center mb-8">
|
||||
<h3 class="text-xl text-gray-600 mb-2 dark:text-white">{{ quiz.name }}</h3>
|
||||
<h3 class="text-xl text-gray-600 mb-2">{{ quiz.name }}</h3>
|
||||
{% if request.session.mode != "learn" %}
|
||||
{% if host_id %}
|
||||
<div class="bg-blue-100 rounded-lg p-4 mb-4 dark:bg-transparent">
|
||||
<h1 class="text-4xl font-bold text-blue-800 dark:text-white">{{ quiz_game.join_code }}</h1>
|
||||
<div class="bg-blue-100 rounded-lg p-4 mb-4">
|
||||
<h1 class="text-4xl font-bold text-blue-800">{{ quiz_game.join_code }}</h1>
|
||||
<div class="w-full flex justify-center my-2">
|
||||
<img src="data:image/png;base64,{{ qr_code_base64 }}" class="h-1/2 w-1/2" alt="QR-Code">
|
||||
</div>
|
||||
<p class="text-sm text-blue-600 mt-1 dark:text-white">Spiel-Code</p>
|
||||
<p class="text-sm text-blue-600 mt-1">Spiel-Code</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -23,7 +23,7 @@
|
||||
<!-- Participant Info -->
|
||||
{% if participant %}
|
||||
<div class="mb-6 text-center">
|
||||
<div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full dark:text-black">
|
||||
<div class="inline-flex items-center bg-green-100 px-4 py-2 rounded-full">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<span>Angemeldet als <b>{{ participant.display_name }}</b></span>
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Participants List -->
|
||||
<div class="mb-6 overflow-y-scroll max-h-96 overflow-x-hidden">
|
||||
<div class="mb-6 overflow-y-scroll max-h-96">
|
||||
<h3 class="font-bold text-lg mb-3 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
@@ -39,7 +39,7 @@
|
||||
Teilnehmer
|
||||
</h3>
|
||||
<ul id="participants-list" class="space-y-2 max-h-60">
|
||||
<li class=" p-3 bg-gray-50 rounded-lg text-center dark:bg-transparent">Warte auf Teilnehmer...</li>
|
||||
<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Warte auf Teilnehmer...</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
}
|
||||
|
||||
if (!participants || participants.length === 0) {
|
||||
list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center dark:bg-transparent">Noch keine Teilnehmer...</li>';
|
||||
list.innerHTML = '<li class="text-gray-500 p-3 bg-gray-50 rounded-lg text-center">Noch keine Teilnehmer...</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,13 +298,13 @@
|
||||
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' +
|
||||
'd="M6 18L18 6M6 6l12 12"/>' +
|
||||
'</svg>' +
|
||||
'<span class="absolute pr-8 -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' +
|
||||
'<span class="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 ' +
|
||||
'bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 ' +
|
||||
'transition-opacity duration-200 whitespace-nowrap">Spieler kicken</span>' +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
return '<li class="dark:bg-transparent p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' +
|
||||
return '<li class="p-3 bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition duration-200 flex items-center justify-between animate-fade-in">' +
|
||||
'<div class="flex items-center">' +
|
||||
'<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">' +
|
||||
'<span class="text-blue-600 font-bold">' + initial.toUpperCase() + '</span>' +
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-8 ">
|
||||
<div class="rounded-md rounded-xl shadow-lg border-3 border-blue-100 p-10">
|
||||
<span class="flex justify-center font-bold p-4 text-center text-1xl lg:text-2xl dark:text-white">Bitte wählen Sie einen Spielmodus aus:</span>
|
||||
<span class="flex justify-center font-bold p-4 text-center text-1xl lg:text-2xl">Bitte wählen Sie einen Spielmodus aus:</span>
|
||||
|
||||
<div class="grid sm:grid-cols-2 place-items-stretch text-center gap-4 p-4">
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-lg mx-auto mt-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-white">
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Möchten Sie ihren Account wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Passwort ändern{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white!">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black">Passwort ändern</h2>
|
||||
<form class="space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
@@ -21,17 +21,17 @@
|
||||
<div class="register">
|
||||
|
||||
{{ form.old_password.label_tag }}
|
||||
<input type="password" name="old_password" placeholder="ALTES PASSWORT" class="dark:text-black" />
|
||||
<input type="password" name="old_password" placeholder="ALTES PASSWORT" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="register">
|
||||
{{ form.new_password1.label_tag }}
|
||||
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" class="dark:text-black" />
|
||||
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" />
|
||||
</div>
|
||||
<div class="register">
|
||||
{{ form.new_password2.label_tag }}
|
||||
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" class="dark:text-black" />
|
||||
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" />
|
||||
</div>
|
||||
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit">Bestätigen</button>
|
||||
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:home' %}" class="register_link">Doch nicht ändern?</a></button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Passwort zurückgesetzt{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black">Passwort zurückgesetzt</h2>
|
||||
<div class="space-y-4">
|
||||
Das Passwort wurde erfolgreich zurückgesetzt! Sie können sich nun mit dem neuen Passwort anmelden!
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black dark:text-white">Passwort zurücksetzen</h2>
|
||||
<h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2>
|
||||
{% if validlink %}
|
||||
<form class="space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
@@ -21,12 +21,11 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="register">
|
||||
|
||||
<span class="dark:text-white">{{ form.new_password1.label_tag }} </span>
|
||||
{{ form.new_password1.label_tag }}
|
||||
<input type="password" name="new_password1" placeholder="NEUES PASSWORT" />
|
||||
</div>
|
||||
<div class="register">
|
||||
<span class="dark:text-white">{{ form.new_password2.label_tag }} </span>
|
||||
{{ form.new_password2.label_tag }}
|
||||
<input type="password" name="new_password2" placeholder="NEUES PASSWORT WIEDERHOLEN" />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12">
|
||||
<div class="w-80 p-6 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 text-center dark:text-white">
|
||||
<h2 class="text-2xl font-black mb-4 dark:text-white">E-Mail wurde gesendet</h2>
|
||||
<p class="text-gray-700 mb-6 dark:text-white">
|
||||
<div class="w-80 p-6 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 text-center">
|
||||
<h2 class="text-2xl font-black mb-4">E-Mail wurde gesendet</h2>
|
||||
<p class="text-gray-700 mb-6">
|
||||
Wenn ein Konto mit dieser E-Mail-Adresse existiert, haben wir dir eine Nachricht mit einem Link zum Zurücksetzen des Passworts gesendet.
|
||||
|
||||
</p><p class="text-gray-700 mb-6 font-bold dark:text-white"> Tipp: Gegebenenfalls Spam überprüfen.</p>
|
||||
</p><p class="text-gray-700 mb-6 font-bold"> Tipp: Gegebenenfalls Spam überprüfen.</p>
|
||||
|
||||
|
||||
<a href="{% url 'accounts:login' %}" class="block w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200">Zur Anmeldung</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Passwort ändern{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90 dark:text-white">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black">Passwort zurücksetzen</h2>
|
||||
<form class="space-y-4" method="post">
|
||||
Gib hier deine E-Mail an und erhalte eine E-Mail, um das Passwort zurückzusetzen.
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<div class="register">
|
||||
{{ form.email.label_tag }}
|
||||
<input type="email" name="email" placeholder="E-MAIL" class="dark:text-black" />
|
||||
<input type="email" name="email" placeholder="E-MAIL" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,3 @@ redis
|
||||
channels_redis
|
||||
python-dotenv # Production server safety
|
||||
qrcode~=7.4.2
|
||||
reportlab~=4.2.0
|
||||
pip-licenses ~= 5.0.0
|
||||
requests
|
||||
requests
|
||||
geopy
|
||||
django-autocomplete-light
|
||||
|
||||
Reference in New Issue
Block a user